Design tab: translatable offer, one vocabulary, reports in the status line

- Offer table: user-facing strings carry the L() marker so xgettext
  extracts them; DesignOffer.hpp, DesignSketchTool.cpp and
  SketchInlineEditor.cpp are listed in localization/i18n/list.txt.
- Offer: model-mode Constrain sits in the same row as the sketch one;
  Interference is wired; Rib shows its R key; a verb that accepts the
  selection but is blocked by the document stays greyed with its reason
  instead of vanishing from the submenu; one refusal wording per verb.
- Extrude infers Join when the profile touches a solid (new
  CadDocument::body_touching_sketch) and on face push/pull; New body in
  free space. Revolve/Sweep/Loft/Boolean use the same result words.
- Interference and volume/area reports go to the status line in mm3/mm2
  instead of modal dialogs; Delete Body no longer asks (it is undoable).
- New feature names match the card header ("Extrude 3"), translated;
  "Coordinate system", "Angle (°)", center/color spelling, translated
  face and length readouts, slot hints say width.
- CAD gizmos in Prepare are selectable only with the CAD feature on; the
  sketch auto-close setting is stored per design.
- Docs: confirm/cancel rules, enabling the feature and MCP in
  design_tab.md; drift-only right-click in interaction-model.md; the
  portability note rewritten to describe the integration as it is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QK4VgguuCAk2hZLWgcjJb9
This commit is contained in:
Claude
2026-09-30 08:34:25 +00:00
parent 829124982d
commit c74ddf8bb5
24 changed files with 662 additions and 582 deletions
+38 -5
View File
@@ -14,10 +14,15 @@ already ships for STEP import.
## Getting started
The Design tab is experimental. It exists only in builds with `SLIC3R_CAD` on, and appears
after **Preferences → CAD feature (experimental)** is ticked and the app restarted. Switched off,
Prepare and every project behave exactly as without it; a project that carries a design keeps
its recipe untouched.
1. Open the **Design** tab.
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.
3. Draw a closed profile, then press **✓ Confirm** in the floating action bar (or `Enter`).
4. With the sketch selected, press `Shift+E` (Extrude).
5. Press **Commit to Plate** to hand the solid to Prepare.
@@ -59,7 +64,8 @@ sketch tools — the app hands you the tools directly.
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.
into a submenu under the family name. A verb that is about the selection but blocked by the
document (no body yet, no second sketch) stays in that submenu, greyed, with its reason.
- 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
@@ -72,6 +78,22 @@ Redo, Variables, Section view, Origin planes, World axes. They live in the toolb
---
## Confirm and cancel
One rule everywhere, in the feature cards and in the sketch alike:
- `Enter` does what **✓** does; `Esc` does what **✗** does. Neither is ever the only way.
- `Esc` steps back exactly one level — a value field, then the gesture in progress, then the
armed tool, then the selection. It never deletes, discards a sketch or rolls back a feature.
- A click on empty space clears the selection. It never applies anything: a pending fillet,
offset, move or array waits for `Enter` or **✓**.
- Right-click on a tool with something pending drops that gesture; with nothing pending it
opens the offer, like anywhere else.
- Switching to another tool applies a pending operation that is complete and valid, then arms
the new tool.
- A value typed into a field that the geometry cannot take (a zero-length side, an arc of more
than 360°) is refused in place: the field stays open and says why.
## Keyboard
Single letters drive sketch tools **while a sketch is open**; Shift+letter drives feature
@@ -100,8 +122,10 @@ selected by the mode, not by whether a sketch session is running.
| `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 |
| `Del` | Delete the selected sketch entities (nothing selected: nothing happens) |
| `Enter` | Apply what is pending; otherwise end the chain; otherwise drop the tool |
| `Esc` | Undo one level: close the field, drop the gesture, drop the tool, clear the selection |
| `Ctrl+Z` / `Ctrl+Y` | Undo / redo the last sketch edit |
### Feature (when no sketch is open)
@@ -151,7 +175,9 @@ kept. With no section on, `F` is Place on Face — lay the picked face flat on t
## 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
Pick the face or plane and press `Shift+S` — the offer opens with the sketch tools. Pressing
`Shift+S` with nothing picked also works: the status line asks for the face or plane, and the
first tool you arm sketches on it. The toolbar and
the offer both carry the sketch tools.
**Entities:** line, polyline, rectangle (corner / centre / oblique / rounded), circle
@@ -290,6 +316,13 @@ edit; the importer warns before you commit to it.
inside the 3MF, so reopening the project restores the editable model rather than a frozen
mesh.
**MCP control** lets an external agent drive the tab through the same kernel the GUI uses.
Start the app with `ORCA_CAD_MCP=1` (socket `/tmp/orca-cad-mcp.sock`) or
`ORCA_CAD_MCP=/path/to.sock`; the CAD feature must be enabled too. Linux and macOS only. A
command that would change the document is refused while the Design tab is busy with it — a
rebuild, an open feature card or a sketch session — so the agent and the user never edit the
same thing at once.
---
## View controls
+4 -3
View File
@@ -20,13 +20,14 @@ design intent that both steps discard. A part modified after slicing should retu
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).
For the integration case in full: [design_tab_upstream_portability.md](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`.
OCCT's CMake: `BUILD_MODULE_ModelingAlgorithms=OFF → ON`. At run time the tab is off until
Preferences → CAD feature (experimental) is enabled.
Measured cost table: [cad_dependency_weight.md](docs/cad_dependency_weight.md).
@@ -55,7 +56,7 @@ circle-line tangency solver abort and the internal-thread reference) are fixed.
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
[design_tab_upstream_portability.md](design_tab_upstream_portability.md) for the
AGPLv3/GPLv3 compatibility argument; this point should be confirmed with upstream explicitly.
## Not verified
+56 -141
View File
@@ -1,162 +1,77 @@
# Design (CAD) tab — upstream integration brief
# Design (CAD) tab — integration surface
**Question:** can the Design tab (sketch-first parametric CAD: sketch → constrain →
extrude/revolve/fillet/hole/thread/shell, multi-body, undo, 3MF persistence) land in
mainline OrcaSlicer?
What the Design tab adds to OrcaSlicer, what it touches that upstream already has, and the
constraints that come with it. The dependency cost is measured separately in
[cad_dependency_weight.md](cad_dependency_weight.md); this page does not repeat its numbers.
**Answer: yes, and the ask is far smaller than previously believed.** OCCT is *already*
an OrcaSlicer dependency. We are not asking upstream to adopt a new library; we are
asking it to widen one it already builds, at a measured cost of **3.77 MiB on Windows**.
## Two gates
> ### Corrections to the 2026-06-21 assessment
> That revision was written before the persistence work landed and got two load-bearing
> facts wrong. Both are corrected here from direct measurement of the branch:
>
> 1. **"The real blocker: OCCT … a dependency mainline OrcaSlicer has never carried."**
> **False.** `deps/OCCT/` exists at the merge-base and upstream links it from
> `Format/STEP.cpp`, `Format/svg.cpp`, and `Shape/TextShape.cpp`. Our entire
> dependency diff is **one line**: `BUILD_MODULE_ModelingAlgorithms=OFF → ON`.
> 2. **"vendored SolveSpace solver … LGPL."** **False.** `src/libslic3r/slvs/LICENSE` is
> **GPL-3.0**, not LGPL. This is fine (see Licensing) but must not be misstated.
>
> It also claimed "no changes to Model" — no longer true; 3MF recipe persistence adds one
> `std::string` to `Model`.
- **Compile time: `SLIC3R_CAD`** (`CMakeLists.txt`, default ON in this tree). OFF builds no
CAD code at all and leaves the OCCT deps prefix identical to upstream. ON also turns on
OCCT's `ModelingAlgorithms` module (`deps/OCCT/OCCT.cmake`).
- **Run time: Preferences → CAD feature (experimental)** (`enable_cad_feature`, default OFF).
Off, the Design page and the MCP control socket are not created, the Prepare-toolbar Sketch
and Primitive gizmos cannot be selected, and the Design-only preferences are hidden: Prepare
behaves as it does without the feature. A project that carries a CAD recipe still loads,
keeps the recipe and writes it back unchanged.
## Measured shape of the change
## Dependencies
Against merge-base `449a4cf9fc` (34 commits ahead):
OCCT is already an upstream dependency (STEP import, SVG, text shapes). The Design tab adds no
library; it widens the OCCT build by one module, which builds three extra toolkits — TKFillet,
TKOffset and TKFeat — of which the first two are linked. The module flag is all-or-nothing, so
TKFeat is built although nothing references it.
| | files | lines |
|---|---:|---:|
| **New files** | 138 | +61,720 |
| **Modified upstream files** | 23 | +457 / −75 |
| **Deleted upstream files** | 0 | — |
The constraint solver is vendored in `src/libslic3r/slvs/`: a self-contained subset of
SolveSpace (`libslvs`), GPL-3.0, no external dependencies. GPLv3 §13 and AGPLv3 §13 permit the
combination with OrcaSlicer's AGPL-3.0.
The 62 kLOC headline is inflated by localization. The feature itself:
## Footprint in upstream files
| area | LOC | files |
|---|---:|---:|
| kernel (`src/libslic3r/`) | 15,828 | 37 |
| GUI (`src/slic3r/`) | 19,544 | 14 |
| tests (Catch2) | 2,567 | 6 |
| i18n (unrelated; strip from the CAD PR) | 23,438 | 77 |
Almost all of the change is new files under `src/libslic3r/CAD/`, `src/slic3r/GUI/CAD/`,
`src/libslic3r/slvs/` and `tests/`. The hooks into existing code are small and null-guarded:
**99.3 % of the diff is new files.** The negotiable surface is 457 added lines across 23
files, and nothing upstream is deleted. The largest single hook is `GLCanvas3D.cpp`
(+110/−2): an `m_design_sketch_tool` member plus render/mouse/key hooks, **every one
already null-guarded** — which is why the compile-time gate below is cheap.
- `GLCanvas3D` — a `DesignSketchTool` pointer plus render, mouse and key hooks, each a no-op
when no Design canvas owns it.
- `MainFrame` — the Design page and the Edit menu routing Undo/Redo to it while it is shown.
- `Plater` — the "no geometry" warning distinguishes a recipe-only project.
- `Model` — one `std::string cad_recipe`, empty for non-CAD projects.
- `bbs_3mf` — reading and writing that string (below).
- `Preferences`, `GUI_App` — the two preferences.
No changes to the slicing pipeline (Print/PrintObject/Layer/GCode), Tab, or the
printer-profile/config system.
Nothing in the slicing pipeline (Print, PrintObject, Layer, GCode), Tab, or the profile and
config system changes.
## The dependency ask, precisely
Not "adopt OCCT" — **widen the existing OCCT build**:
```diff
- -DBUILD_MODULE_ModelingAlgorithms=OFF
+ -DBUILD_MODULE_ModelingAlgorithms=ON
```
Cost, measured from the shipped Windows artifact (42 OCCT DLLs, 45.43 MiB total):
| toolkit | size | note |
|---|---:|---|
| `TKFillet.dll` | 2.02 MiB | only exists with the flag ON |
| `TKOffset.dll` | 1.75 MiB | only exists with the flag ON |
| **delta** | **3.77 MiB** | Windows only (OCCT is Shared on Win, Static elsewhere) |
`TKBool` is *not* part of the delta — upstream's `DataExchange` already pulls it in
transitively. On macOS/Linux OCCT links statically, so the cost is only the code actually
referenced, not a 3.77 MiB floor.
**Unmeasured, and we should measure before the call:** clean-deps build-time delta with
the flag ON vs OFF, and the resulting CI runner-minute cost. Do not guess these at him.
## Licensing
- Vendored solver `src/libslic3r/slvs/` — **GPL-3.0**, 9,339 LOC, © Jonathan Westhues,
a self-contained subset of SolveSpace (`libslvs`). No external dependencies.
- OrcaSlicer — **AGPL-3.0** (`LICENSE.txt`).
GPLv3 §13 expressly permits combining a GPLv3 work with an AGPLv3 work; AGPLv3 §13 grants
the converse. The combined work is distributable under AGPL-3.0 with the solver's GPLv3
terms preserved. This is a favourable direction (GPLv3 → into an AGPLv3 project), but it
is a point to **confirm explicitly with upstream**, not to assert unilaterally.
Open question for SoftFever: keep the solver **vendored** (current: pinned, no submodule,
no external build) or move it to `deps/` as a fetched external? Vendoring costs us
upstream-sync burden; `deps/` costs build complexity.
## The one irreversible decision: the 3MF format
Persistence adds an **optional** archive entry and one field:
```cpp
// Model.hpp
std::string cad_recipe; // empty for non-CAD projects
```
## The 3MF entry
```
Metadata/orca_cad.bin // written only when cad_recipe is non-empty
Metadata/orca_cad.bin written only when cad_recipe is non-empty
Metadata/SnapOrca_cad.bin legacy name: read, never written; loses to orca_cad.bin
```
Readers that do not know the entry ignore it; writers skip it entirely when empty. So
existing projects are bit-identical and old readers are unaffected. Good.
Readers that do not know the entry ignore it and writers skip it when the recipe is empty, so
projects without a design are byte-identical and older readers are unaffected. The reader caps
the entry at 1 GiB.
**But the moment upstream ships this, it owns forward-compatibility forever.** Three
things should be settled *before* the first release, because none can be changed after:
The entry is a cereal `PortableBinaryArchive`, portable across endianness and word size. Its
layout is versioned:
1. **Name.** Renamed to `Metadata/orca_cad.bin`.
2. **Encoding.** The recipe is an opaque **cereal `PortableBinaryArchive`** blob whose
layout is the field order of `CadFeature::serialize`. Portable across endianness and
word size — *not* across a field reorder. Append-only is currently a convention held by
discipline, not by any check.
3. **Embedded BRep.** `Import` features embed OCCT's ASCII BRep for the imported solid,
which couples saved project files to an OCCT BRep revision. Alternative: re-import from
the source STEP and store only a reference. Worth deciding deliberately.
- **v5 and later** frame every feature separately, so fields are appended to
`CadFeature::serialize` without a version bump and an older reader skips what it does not
know. Optional document-level data (variables, body names, origin, colours, the
auto-close setting) follows in trailing blocks that a reader stops at cleanly when absent.
- **v4** is the flat layout, read by the frozen `CadFeature::load_flat_v4`, which must never
change.
**Concrete gap we should close before the call.** `test_caddocument.cpp` covers the
in-memory round-trip and correctly refuses a version-999 blob — but there is **no
checked-in v1 fixture on disk**. A reordered field in `CadFeature::serialize` would pass
the entire suite while silently breaking every previously-saved project. Ship a golden
`.bin` fixture generated today plus a test that loads it; that is the only thing that will
hold the format still once real users have files.
Checked-in fixtures `tests/data/cad_recipe_v3.bin`, `cad_recipe_v4.bin` and
`cad_recipe_v5.bin` hold the format still: `test_caddocument.cpp` loads them and checks what
they contain, so a reordered field fails the suite instead of breaking saved projects.
## Proposed PR decomposition
`Import` features embed OCCT's ASCII BRep of the imported solid, which ties a saved project to
a BRep revision OCCT can read.
35 kLOC in one PR is not reviewable. Behind the flag, slices 1–4 are behaviour-neutral for
existing users:
## Undo
1. **Build gate + OCCT flag + Windows packaging guard.** `-DSLIC3R_CAD=ON/OFF`, default
**OFF**. Flips `ModelingAlgorithms=ON`. Includes the guard that asserts every linked
OCCT toolkit has a shipped DLL (already on both forks: `546cef5f42`). ← *this is what
makes SoftFever's "parallel build" a one-line CI matrix entry.*
2. **Vendored `slvs` solver** + its Catch2 tests. No GUI, no OCCT.
3. **CAD kernel** (`CadDocument`, `SketchEngine`, `GeometryEngine`, `Sketch*`) + kernel
tests. Headless, no GUI.
4. **3MF recipe persistence** + golden-fixture regression test.
5. **GUI Design tab** (`DesignPanel`, `DesignCanvas`, `DesignSketchTool`, `GLGizmoSketch`)
+ the 23 upstream hooks.
## Agenda for the call
Questions only SoftFever can answer:
- Does OrcaSlicer *want* to be a CAD-integrated slicer? (Strategic; everything else is mechanical.)
- Default of `SLIC3R_CAD` at merge time, and when it flips ON.
- Vendored solver vs `deps/` external; and confirmation of the GPLv3/AGPLv3 combination.
- Project-file format: neutral name, encoding, embedded-BRep policy, and who owns v1 forward-compat.
- Undo/redo: the Design tab has its own stack; integrate with Orca's snapshot system or keep separate?
- Does he want the i18n work (Romanian, +23 kLOC) as a wholly separate PR? (Yes, almost certainly.)
## Verdict
Portability **high**. The prior "does upstream want OCCT" framing was wrong — OCCT is
already there. What remains is a 3.77 MiB dependency widening, a compile-time gate that
the existing null-guards make cheap, and one file-format decision that must be made before
the first release rather than after.
---
*Revised 2026-07-10 from direct measurement of `cad-mainline` @ `546cef5f42` vs upstream
merge-base `449a4cf9fc`. Supersedes the 2026-06-21 read-only assessment.*
The Design tab keeps its own history (`CadDocument` checkpoints), separate from the Plater's
snapshot stack. The Edit menu's Undo/Redo drive whichever of the two belongs to the page on
screen.
+7 -5
View File
@@ -72,10 +72,11 @@ which is what makes the destructive path unrepresentable rather than merely unli
own handlers so it can consume the event before them:
```cpp
RIGHT_DOWN: remember the press position and the clock, then Skip() // the canvas still seeds the orbit
RIGHT_DOWN: remember the press position, clear `travelled`, then Skip() // the canvas still seeds the orbit
MOTION: travelled |= drift from the press > 3 px // an orbit that came back still orbited
RIGHT_UP: terminated = sketch_tool.take_right_consumed(); // read-and-clear, always
is_click = drift <= 3 px && dt <= 200 ms; // both budgets, or it was navigation
is_click = !travelled && drift <= 3 px; // otherwise it was navigation
if (callback && !terminated && !inline_busy && is_click) {
select_at_screen(press.x, press.y); // raycast at the PRESS, not the release
on_context_menu(ClientToScreen(press));
@@ -84,8 +85,9 @@ RIGHT_UP: terminated = sketch_tool.take_right_consumed(); // read-and
Skip(); // orbit / pan / the handlers underneath
```
Two independent budgets because the two failure modes are independent: drift alone still popped a
menu at the end of a slow, careful orbit. `take_right_consumed()` is how a right-click that
Only distance decides, never time (charter §6.2: no timing-dependent gestures): a slow, deliberate
right-click is still a click. The drift is tracked over the whole press, not just at release, so a
careful orbit that ends where it started does not pop a menu. `take_right_consumed()` is how a right-click that
already meant something to the armed sketch tool (terminate a chain, drop an edit-op) declines to
also mean "open a menu".
@@ -107,7 +109,7 @@ also mean "open a menu".
| **Transient — popup menu** | run the entry | — | close the menu | run the highlighted entry |
| **any** | — | — | *never* deletes, discards or rolls back | — |
Right-hold-and-drag is not in the table on purpose: past 3 px or 200 ms it is navigation, and
Right-hold-and-drag is not in the table on purpose: once it has travelled past 3 px it is navigation, and
navigation does not transition the state machine.
## 4. Visual scaffolding
+15 -5
View File
@@ -42,6 +42,12 @@ def cstr(s):
return '"' + s.replace("\\", "\\\\").replace('"', '\\"') + '"'
def tstr(s):
# User-facing text: wrapped in the no-op L() marker so xgettext puts it in the catalogue.
# The DesignPanel translates it at use; without the marker it compiles and never translates.
return "nullptr" if s is None else "L(" + cstr(s) + ")"
def main():
A = json.load(open(ATLAS, encoding="utf-8"))
sels = [s["id"] for s in A["selections"]]
@@ -61,6 +67,10 @@ def main():
"",
"#include <cstdint>",
"",
"#ifndef L",
"#define L(s) s // gettext marker, as in slic3r/GUI/I18N.hpp",
"#endif",
"",
"namespace Slic3r { namespace GUI {",
"",
"// What the viewport has selected. Ordered as in tool_atlas.json; the bitmask in",
@@ -83,7 +93,7 @@ def main():
"// nullptr -> kernel support exists, no GUI path yet (row shows disabled)",
"struct OfferVerb {",
" const char* id;",
" const char* name; // drawing-office word (L10); translated at use with wxGetTranslation",
" const char* name; // drawing-office word (L10); marked L(); translated at use",
" int row; // 0..7, the ratified index — NEVER reorder",
" const char* key; // shortcut shown in the row, or nullptr",
" const char* action;",
@@ -106,7 +116,7 @@ def main():
"static const char* const kOfferRowNames[] = {",
]
for s in A["slots"]:
lines.append(f' "{s["label"]}",')
lines.append(f' {tstr(s["label"])},')
lines += [
"};",
f"static const int kOfferRowCount = {len(slots)};",
@@ -131,12 +141,12 @@ def main():
n = v.get("needs") or {}
lines.append(
" {%s, %s, %d, %s, %s, %s, 0x%08xu, %d, %d, %s, %s, %s, %s, %s}," % (
cstr(v["id"]), cstr(v["name"]), slots.index(v["slot"]),
cstr(v.get("key")), cstr(v.get("action")), cstr(v.get("refusal")),
cstr(v["id"]), tstr(v["name"]), slots.index(v["slot"]),
cstr(v.get("key")), cstr(v.get("action")), tstr(v.get("refusal")),
mask, n.get("bodies", 0), n.get("sketches", 0),
"true" if n.get("sheet") else "false",
"true" if v.get("mode") == "sketch" else "false",
cstr(v.get("family")), cstr(v.get("icon")), cstr(v.get("hint"))))
tstr(v.get("family")), cstr(v.get("icon")), tstr(v.get("hint"))))
lines += [
"};",
f"static const int kOfferVerbCount = {len(A['verbs'])};",
+38 -36
View File
@@ -326,7 +326,7 @@
"id": "rib",
"name": "Rib",
"slot": "add",
"key": null,
"key": "R",
"feature": "Rib",
"mcp": "rib",
"accepts": [
@@ -343,7 +343,7 @@
},
{
"id": "boolean",
"name": "Union",
"name": "Join",
"slot": "add",
"key": "Shift+B",
"feature": "Boolean",
@@ -354,7 +354,7 @@
"needs": {
"bodies": 2
},
"refusal": "Boolean needs two bodies — create or import a second solid",
"refusal": "Boolean needs two bodies — add or import a second one",
"gui": true,
"action": "btn:bool#0",
"icon": "design_boolean",
@@ -373,7 +373,7 @@
"needs": {
"bodies": 2
},
"refusal": "Boolean needs two bodies — create or import a second solid",
"refusal": "Boolean needs two bodies — add or import a second one",
"gui": true,
"action": "btn:bool#1",
"mode": null,
@@ -393,7 +393,7 @@
"needs": {
"bodies": 2
},
"refusal": "Boolean needs two bodies — create or import a second solid",
"refusal": "Boolean needs two bodies — add or import a second one",
"gui": true,
"action": "btn:bool#2",
"mode": null,
@@ -483,7 +483,7 @@
"needs": {
"sheet": true
},
"refusal": "target is not a sheet body",
"refusal": "Thicken Surface needs a surface body — make one with a Surface tool first",
"gui": true,
"action": "fly:surface#5",
"icon": "design_thicken",
@@ -507,7 +507,7 @@
"gui": true,
"action": "key:S+H",
"icon": "design_hole",
"hint": "Drill a hole, centred on a picked face or placed on a plane"
"hint": "Drill a hole, centerd on a picked face or placed on a plane"
},
{
"id": "thread",
@@ -543,7 +543,7 @@
"needs": {
"bodies": 1
},
"refusal": "Shell needs a solid body",
"refusal": "Shell needs a solid body — add or import one first",
"gui": true,
"action": "key:S+K",
"icon": "design_shell",
@@ -569,7 +569,7 @@
"needs": {
"bodies": 1
},
"refusal": "Create a solid body to cut first",
"refusal": "Cut needs a solid body — add or import one first",
"gui": true,
"action": "key:S+X",
"icon": "design_cut",
@@ -594,7 +594,7 @@
"needs": {
"bodies": 1
},
"refusal": "Split needs a solid body",
"refusal": "Split needs a solid body — add or import one first",
"gui": false,
"action": null,
"icon": null,
@@ -677,7 +677,7 @@
"needs": {
"sheet": true
},
"refusal": "target is not a sheet body",
"refusal": "Surface Offset needs a surface body — make one with a Surface tool first",
"gui": true,
"action": "fly:surface#4",
"icon": "design_offset",
@@ -699,7 +699,7 @@
"needs": {
"bodies": 1
},
"refusal": "Create a solid body to pattern first",
"refusal": "Pattern needs a solid body — add or import one first",
"gui": true,
"action": "btn:pat#0",
"icon": "design_array",
@@ -721,7 +721,7 @@
"needs": {
"bodies": 1
},
"refusal": "Create a solid body to pattern first",
"refusal": "Pattern needs a solid body — add or import one first",
"gui": true,
"action": "btn:pat#1",
"mode": null,
@@ -768,7 +768,7 @@
"needs": {
"bodies": 1
},
"refusal": "Pattern on curve needs a body and a curve",
"refusal": "Pattern on Curve needs a body and a curve",
"gui": false,
"action": null,
"icon": null,
@@ -816,11 +816,11 @@
"needs": {
"bodies": 2
},
"refusal": "A mate needs two coordinate systems",
"refusal": "Mate needs two coordinate systems — create them first",
"gui": true,
"action": "fly:placement#2",
"icon": "design_c_coincident",
"hint": "Assembly: align two CoordSys features (fastened, planar, revolute, slider, cylindrical)"
"hint": "Assembly: align two coordinate systems (fastened, planar, revolute, slider, cylindrical)"
},
{
"id": "align",
@@ -835,7 +835,7 @@
"needs": {
"bodies": 1
},
"refusal": "Align needs a body",
"refusal": "Align needs a body — add or import one first",
"gui": false,
"action": null,
"icon": null,
@@ -885,7 +885,7 @@
},
{
"id": "coordsys_v",
"name": "Coord Sys",
"name": "Coordinate system",
"slot": "reference",
"key": "Shift+C",
"feature": "CoordSys",
@@ -972,7 +972,7 @@
},
{
"id": "mass_props",
"name": "Mass",
"name": "Volume and area",
"slot": "reference",
"key": null,
"feature": null,
@@ -1003,14 +1003,15 @@
"feature": null,
"mcp": "check_interference",
"accepts": [
"body_solid",
"bodies_2"
],
"needs": {
"bodies": 2
},
"refusal": null,
"gui": false,
"action": null,
"refusal": "Interference needs at least two bodies",
"gui": true,
"action": "btn:interference",
"icon": null,
"hint": "Check whether two bodies overlap — reports, changes nothing"
},
@@ -1082,7 +1083,7 @@
},
{
"id": "colour",
"name": "Colour",
"name": "Color",
"slot": "modify",
"key": null,
"feature": null,
@@ -1104,7 +1105,7 @@
"gui": true,
"action": "btn:colour",
"icon": "color_palette",
"hint": "Set the selected body's display colour"
"hint": "Set the selected body's display color"
},
{
"id": "delete",
@@ -1227,7 +1228,7 @@
},
{
"id": "sk_rect_center",
"name": "Centre rectangle",
"name": "Center rectangle",
"slot": "create",
"key": null,
"feature": "Sketch",
@@ -1296,7 +1297,7 @@
},
{
"id": "sk_circle",
"name": "Centre circle",
"name": "Center circle",
"slot": "create",
"key": "C",
"feature": "Sketch",
@@ -1411,7 +1412,7 @@
},
{
"id": "sk_arc_center",
"name": "Centre-point arc",
"name": "Center-point arc",
"slot": "create",
"key": null,
"feature": "Sketch",
@@ -1453,7 +1454,7 @@
"action": "key:S",
"family": "Slot",
"icon": "design_slot",
"hint": "Slot — two centerline ends, then end radius"
"hint": "Slot — two centerline ends, then the width"
},
{
"id": "sk_slot_arc",
@@ -1567,7 +1568,7 @@
"mode": "sketch",
"family": "Polygon",
"icon": "design_polygon",
"hint": "Triangle — click centre, then a vertex"
"hint": "Triangle — click center, then a vertex"
},
{
"id": "sk_poly_4",
@@ -1590,7 +1591,7 @@
"mode": "sketch",
"family": "Polygon",
"icon": "design_polygon",
"hint": "Square — click centre, then a vertex"
"hint": "Square — click center, then a vertex"
},
{
"id": "sk_poly_5",
@@ -1613,7 +1614,7 @@
"mode": "sketch",
"family": "Polygon",
"icon": "design_polygon",
"hint": "Pentagon — click centre, then a vertex"
"hint": "Pentagon — click center, then a vertex"
},
{
"id": "sk_polygon",
@@ -1636,7 +1637,7 @@
"action": "btn:poly#6",
"family": "Polygon",
"icon": "design_polygon",
"hint": "Hexagon — click centre, then a vertex"
"hint": "Hexagon — click center, then a vertex"
},
{
"id": "sk_poly_8",
@@ -1659,7 +1660,7 @@
"mode": "sketch",
"family": "Polygon",
"icon": "design_polygon",
"hint": "Octagon — click centre, then a vertex"
"hint": "Octagon — click center, then a vertex"
},
{
"id": "sk_poly_12",
@@ -1682,7 +1683,7 @@
"mode": "sketch",
"family": "Polygon",
"icon": "design_polygon",
"hint": "Dodecagon — click centre, then a vertex"
"hint": "Dodecagon — click center, then a vertex"
},
{
"id": "sk_poly_inscribed",
@@ -2048,7 +2049,7 @@
{
"id": "constrain",
"name": "Constrain sketch",
"slot": "modify",
"slot": "reference",
"key": null,
"feature": "Sketch",
"mcp": null,
@@ -2067,7 +2068,8 @@
"note": [
"Same verb, model-mode vocabulary: offered when a SKETCH is selected (bit 14, SkLoop), the",
"state a user is in right after finishing one. Without this row the only way in was the",
"toolbar icon, and constraints read as absent — see the Onshape-comparison report."
"toolbar icon, and constraints read as absent — see the Onshape-comparison report.",
"It sits in the Reference row, the address the sketch-mode Constrain has: one verb, one row."
]
},
{
+2
View File
@@ -298,7 +298,9 @@ src/slic3r/GUI/AMSDryControl.cpp
src/slic3r/GUI/AMSDryControl.hpp
src/libslic3r/PresetBundle.cpp
src/slic3r/GUI/CAD/DesignPanel.cpp
src/slic3r/GUI/CAD/DesignSketchTool.cpp
src/slic3r/GUI/CAD/SketchInlineEditor.cpp
src/slic3r/GUI/CAD/DesignOffer.hpp
src/slic3r/GUI/Gizmos/GLGizmoPrimitive.cpp
src/slic3r/GUI/Gizmos/GLGizmoSketch.cpp
src/slic3r/GUI/KeyChord.cpp
+2 -1
View File
@@ -95,7 +95,8 @@ def load_table():
f.append(cur.strip())
if len(f) < 12:
continue
lit = lambda s: None if s == "nullptr" else s.strip('"')
# User-facing fields are wrapped in the L("...") gettext marker; strip it.
lit = lambda s: None if s == "nullptr" else re.sub(r'^L\((.*)\)$', r'\1', s).strip('"')
out.append({"id": lit(f[0]), "name": lit(f[1]), "row": int(f[2]), "key": lit(f[3]),
"action": lit(f[4]), "accepts": int(f[6].rstrip("u"), 0),
"need_bodies": int(f[7]), "need_sketches": int(f[8]),
+25 -2
View File
@@ -7,6 +7,7 @@
#include <Standard_Failure.hxx>
#include <BRepBuilderAPI_MakeWire.hxx>
#include <BRepExtrema_DistShapeShape.hxx>
#include <BRepBuilderAPI_MakeEdge.hxx>
#include <BRepBuilderAPI_MakeFace.hxx>
#include <BRepBuilderAPI_MakePolygon.hxx>
@@ -1975,6 +1976,7 @@ void CadDocument::clear()
display_tri_face.clear();
display_tri_body.clear();
origin_from_recipe = false;
auto_close_loops = true;
// Variables are document state like the features: left behind, the previous project's
// variables were written into the next project's recipe.
variables.clear();
@@ -2456,6 +2458,24 @@ TopoDS_Wire CadDocument::build_sketch_wire(const CadFeature& sketch, bool closed
return prof.to_occt_wire(sketch.plane);
}
int CadDocument::body_touching_sketch(int sketch_ref) const
{
if (sketch_ref < 0 || sketch_ref >= int(features.size())
|| features[sketch_ref].type != CadFeatureType::Sketch)
return -1;
TopoDS_Face face;
try { face = build_sketch_face(features[sketch_ref]); } catch (const std::exception&) { return -1; }
if (face.IsNull()) return -1;
for (int i = int(bodies.size()) - 1; i >= 0; --i) { // newest first: the likeliest target
const TopoDS_Shape& b = bodies[i].shape;
if (b.IsNull() || is_sheet_shape(b)) continue;
BRepExtrema_DistShapeShape d(face, b);
if (d.IsDone() && d.Value() <= 1e-4)
return i;
}
return -1;
}
TopoDS_Face CadDocument::build_sketch_face(const CadFeature& sketch) const
{
if (!sketch.entities.empty()) {
@@ -3932,6 +3952,7 @@ void CadDocument::route_feature(std::vector<CadBody>& bodies, const CadFeature&
bool CadDocument::recompute()
{
error.clear();
set_sketch_auto_close(auto_close_loops); // this document's weld rule, not the last one's
detect_mate_conflicts();
std::vector<CadBody> built;
// Did any feature in this document even ASK for a solid? A document made only of sketches
@@ -4210,7 +4231,7 @@ std::string CadDocument::serialize_recipe() const
std::ostringstream xos;
{
cereal::BinaryOutputArchive xa(xos);
xa(modeling_origin.x(), modeling_origin.y(), modeling_origin.z(), colours);
xa(modeling_origin.x(), modeling_origin.y(), modeling_origin.z(), colours, auto_close_loops);
}
std::string xb = xos.str();
uint32_t xlen = static_cast<uint32_t>(xb.size());
@@ -4309,9 +4330,11 @@ bool CadDocument::deserialize_recipe(const std::string& blob)
std::istringstream xs(xbuf);
cereal::BinaryInputArchive xa(xs);
double ox = 0, oy = 0, oz = 0;
xa(ox, oy, oz, colours);
bool weld = true;
xa(ox, oy, oz, colours, weld);
modeling_origin = Vec3d(ox, oy, oz);
origin_from_recipe = true;
auto_close_loops = weld;
} catch (...) {
colours.clear(); // older project: the caller's origin stands
}
+8
View File
@@ -539,6 +539,11 @@ public:
// printer must keep the origin it was made with or its datums move and its sketches do not.
Vec3d modeling_origin{Vec3d::Zero()};
bool origin_from_recipe{false}; // modeling_origin came from the loaded project
// Weld sketch endpoints within kSketchJoinTol (the "Auto-close sketch loops" preference, taken
// when the document is started). A property of the DOCUMENT, saved with it: as a machine-wide
// preference it made one project rebuild into a closed solid on one computer and an open
// loop on another. recompute() pushes it into the kernel.
bool auto_close_loops{true};
// Tessellation quality, matched to Orca's OWN STEP importer (Format/STEP.hpp defaults:
// linear 0.003, angular 0.5 rad) so a body modelled here reaches the screen at the same
@@ -799,6 +804,9 @@ public:
// tessellate the result into out_mesh, WITHOUT modifying features/body/
// display_mesh. Returns false (with err set) if the candidate is invalid.
// Used by the Design tab to show a translucent ghost before Confirm.
// The solid body the closed profile of sketch `sketch_ref` lies on or touches, or -1. Drives
// the Extrude default: a profile drawn on a body joins it, one in free space is a new body.
int body_touching_sketch(int sketch_ref) const;
bool preview(const CadFeature& candidate, TriangleMesh& out_mesh, std::string& err) const;
// Same, but also returns the per-body meshes (in `bodies` order; the candidate may append
// one), so the GUI can apply its display-only per-body Move transforms to the ghost and keep
+9 -6
View File
@@ -47,18 +47,21 @@
#include <TopoDS_Wire.hxx>
#include <GeomAPI_IntCS.hxx>
#include <map>
#include <atomic>
#include <tuple>
#include <stdexcept>
namespace Slic3r {
// Single source of truth for the weld tolerance the viewport and the kernel share.
// Defaults ON so headless/kernel-only callers keep welding; the GUI pushes the
// "auto_close_sketch_loops" preference in via set_sketch_auto_close().
static bool s_auto_close = true;
// Single source of truth for the weld tolerance the viewport and the kernel share. Defaults ON
// so headless/kernel-only callers keep welding. It is the DOCUMENT's setting
// (CadDocument::auto_close_loops, saved with the recipe), pushed in by CadDocument::recompute:
// the same project must rebuild into the same solid on every machine. Atomic because the GUI
// rebuilds on a worker thread while the viewport reads it.
static std::atomic<bool> s_auto_close{true};
double sketch_join_tol() { return s_auto_close ? kSketchJoinTol : 0.0; }
void set_sketch_auto_close(bool on) { s_auto_close = on; }
double sketch_join_tol() { return s_auto_close.load() ? kSketchJoinTol : 0.0; }
void set_sketch_auto_close(bool on) { s_auto_close.store(on); }
// ---- SketchPlane ----
+3 -3
View File
@@ -90,9 +90,9 @@ inline constexpr double kSketchJoinTol = 1e-3; // mm
// Effective sketch joint tolerance. ONE value for the viewport (region_loops /
// loop_report / connected_loop) and the kernel (entities_to_wires): if these ever
// disagree again, the viewport shades a region closed that the kernel refuses to
// build, which is how a sketch got extruded into the wrong solid. The GUI pushes
// the "auto_close_sketch_loops" preference in via set_sketch_auto_close(); the
// kernel defaults to ON so headless/kernel-only callers keep welding.
// build, which is how a sketch got extruded into the wrong solid. The document's
// own setting (CadDocument::auto_close_loops) is pushed in via set_sketch_auto_close();
// the kernel defaults to ON so headless/kernel-only callers keep welding.
double sketch_join_tol();
void set_sketch_auto_close(bool on);
+8 -1
View File
@@ -1961,7 +1961,14 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
else if (boost::algorithm::iequals(name, ORCA_CAD_RECIPE_FILE)
|| boost::algorithm::iequals(name, LEGACY_CAD_RECIPE_FILE)) {
// Restore the editable CAD recipe (optional; absent in non-CAD projects).
if (stat.m_uncomp_size > 0) {
// The current name wins over the legacy one whichever the archive lists
// first, and the size the archive claims is capped before it is allocated.
constexpr mz_uint64 kMaxCadRecipe = mz_uint64(1) << 30; // 1 GiB
const bool legacy = boost::algorithm::iequals(name, LEGACY_CAD_RECIPE_FILE);
if (stat.m_uncomp_size > kMaxCadRecipe) {
BOOST_LOG_TRIVIAL(error) << "3MF: CAD recipe of " << stat.m_uncomp_size
<< " bytes exceeds the limit; not loaded";
} else if (stat.m_uncomp_size > 0 && !(legacy && !model.cad_recipe.empty())) {
std::string buf((size_t)stat.m_uncomp_size, '\0');
if (mz_zip_reader_extract_to_mem(&archive, stat.m_file_index, buf.data(), buf.size(), 0))
model.cad_recipe = std::move(buf);
+106 -101
View File
@@ -10,6 +10,10 @@
#include <cstdint>
#ifndef L
#define L(s) s // gettext marker, as in slic3r/GUI/I18N.hpp
#endif
namespace Slic3r { namespace GUI {
// What the viewport has selected. Ordered as in tool_atlas.json; the bitmask in
@@ -48,7 +52,7 @@ inline uint32_t offer_bit(OfferSel s) { return 1u << int(s); }
// nullptr -> kernel support exists, no GUI path yet (row shows disabled)
struct OfferVerb {
const char* id;
const char* name; // drawing-office word (L10); translated at use with wxGetTranslation
const char* name; // drawing-office word (L10); marked L(); translated at use
int row; // 0..7, the ratified index — NEVER reorder
const char* key; // shortcut shown in the row, or nullptr
const char* action;
@@ -69,118 +73,119 @@ struct OfferVerb {
// Row labels, in ratified order.
static const char* const kOfferRowNames[] = {
"Create",
"Add material",
"Remove",
"Fillet / chamfer / draft",
"Repeat",
"Transform",
"Reference",
"Modify",
L("Create"),
L("Add material"),
L("Remove"),
L("Fillet / chamfer / draft"),
L("Repeat"),
L("Transform"),
L("Reference"),
L("Modify"),
};
static const int kOfferRowCount = 8;
static const OfferVerb kOfferVerbs[] = {
{"sketch", "Sketch", 0, "Shift+S", "key:S+S", "Click a face or a reference plane in the viewport, then a sketch tool", 0x00000403u, 0, 0, false, false, nullptr, "design_sketch", "Click a face or a reference plane, then pick a drawing tool"},
{"extrude", "Extrude", 1, "Shift+E", "key:S+E", "Create a sketch, or pick a solid face, first", 0x00004002u, 0, 0, false, false, nullptr, "design_extrude", "Extrude a sketch profile, or push/pull a picked face"},
{"revolve", "Revolve", 1, "Shift+R", "key:S+R", "Create a sketch profile to revolve first", 0x00004000u, 0, 0, false, false, nullptr, "design_revolve", "Revolve a profile about an axis"},
{"sweep", "Sweep", 1, "Shift+W", "key:S+W", "Create a profile sketch to sweep first", 0x00004000u, 0, 2, false, false, nullptr, "design_sweep", "Sweep a profile along a path"},
{"loft", "Loft", 1, "Shift+L", "key:S+L", "Create at least two profile sketches to loft", 0x00004000u, 0, 2, false, false, nullptr, "design_loft", "Loft (skin) between two or more profiles"},
{"thicken", "Thicken", 1, nullptr, "fly:material#4", "Thicken needs a solid body — add or import one first", 0x0000000au, 1, 0, false, false, nullptr, "design_thicken", "Offset a solid face into a thin plate (new body)"},
{"rib", "Rib", 1, nullptr, "fly:material#5", "Rib needs a solid body — add or import one first", 0x00010000u, 1, 0, false, false, nullptr, "design_rib", "Grow a thin wall from an open sketch line, fused to a body"},
{"boolean", "Union", 1, "Shift+B", "btn:bool#0", "Boolean needs two bodies — create or import a second solid", 0x00000200u, 2, 0, false, false, nullptr, "design_boolean", "Fuse the tool body into the target — one solid, no seam"},
{"bool_subtract", "Subtract", 1, nullptr, "btn:bool#1", "Boolean needs two bodies — create or import a second solid", 0x00000200u, 2, 0, false, false, nullptr, "design_boolean", "Cut the tool body out of the target"},
{"bool_intersect", "Intersect", 1, nullptr, "btn:bool#2", "Boolean needs two bodies — create or import a second solid", 0x00000200u, 2, 0, false, false, nullptr, "design_boolean", "Keep only where the two bodies overlap"},
{"surf_extrude", "Surface Extrude", 1, "Shift+G", "key:S+G", "Create a sketch first", 0x00004000u, 0, 0, false, false, nullptr, "design_extrude", "Extrude a sketch into a sheet body (no end caps)"},
{"surf_revolve", "Surface Revolve", 1, nullptr, "fly:surface#1", "Create a sketch profile to revolve first", 0x00004000u, 0, 0, false, false, nullptr, "design_revolve", "Revolve a sketch profile into a sheet body"},
{"surf_loft", "Surface Loft", 1, nullptr, "fly:surface#2", "Create at least two profile sketches to loft", 0x00004000u, 0, 2, false, false, nullptr, "design_loft", "Loft (skin) between 2+ profiles, open (no end caps)"},
{"surf_fill", "Surface Fill", 1, nullptr, "fly:surface#3", "Create a closed sketch first", 0x00004000u, 0, 0, false, false, nullptr, "design_surface", "Fill a sketch boundary with a smooth face"},
{"thicken_surf", "Thicken Surface", 1, nullptr, "fly:surface#5", "target is not a sheet body", 0x00000100u, 0, 0, true, false, nullptr, "design_thicken", "Thicken a sheet body into a solid"},
{"hole", "Hole", 2, "Shift+H", "key:S+H", "Pick a face or a plane to drill into", 0x00000402u, 1, 0, false, false, nullptr, "design_hole", "Drill a hole, centred on a picked face or placed on a plane"},
{"thread", "Thread", 2, "Shift+T", "key:S+T", "Pick a cylindrical surface (bore / outer) or a circular edge for a thread", 0x00000024u, 1, 0, false, false, nullptr, "design_thread", "Thread a cylindrical surface (inner bore / outer) or a circular edge"},
{"shell", "Shell", 2, "Shift+K", "key:S+K", "Shell needs a solid body", 0x00000082u, 1, 0, false, false, nullptr, "design_shell", "Hollow the body to a wall thickness, opening a picked face"},
{"cut", "Cut", 2, "Shift+X", "key:S+X", "Create a solid body to cut first", 0x000004feu, 1, 0, false, false, nullptr, "design_cut", "Trim the body with a plane — drag the offset arrow; keep one half or both"},
{"split", "Split", 2, nullptr, nullptr, "Split needs a solid body", 0x000000feu, 1, 0, false, false, nullptr, nullptr, "Split the body along a picked face into two solids"},
{"fillet", "Fillet", 3, "Shift+F", "btn:dress#0", "Pick an edge to round", 0x000000b2u, 1, 0, false, false, nullptr, "design_filletedge", "Pick an edge, then drag the radius arrow or type it"},
{"chamfer", "Chamfer", 3, nullptr, "btn:dress#1", "Pick an edge to bevel", 0x000000b2u, 1, 0, false, false, nullptr, "design_chamfer", "Pick an edge, then drag the distance arrow or type it"},
{"draft", "Draft", 3, "Shift+D", "key:S+D", "Pick a face to taper", 0x0000000au, 1, 0, false, false, nullptr, "design_draft", "Tilt a picked face by a draft angle"},
{"surf_offset", "Surface Offset", 3, nullptr, "fly:surface#4", "target is not a sheet body", 0x00000100u, 0, 0, true, false, nullptr, "design_offset", "Offset a sheet body's shell by a signed distance"},
{"pattern", "Linear pattern", 4, "Shift+N", "btn:pat#0", "Create a solid body to pattern first", 0x00006082u, 1, 0, false, false, nullptr, "design_array", "Repeat the body along a direction — drag the spacing, set the count"},
{"pattern_circular", "Circular pattern", 4, nullptr, "btn:pat#1", "Create a solid body to pattern first", 0x00006082u, 1, 0, false, false, nullptr, "design_polararray", "Repeat the body around an axis — set the count and sweep"},
{"mirror", "Mirror", 4, "Shift+Z", "key:S+Z", "Mirror needs a body — add or import one first", 0x000004feu, 1, 0, false, false, nullptr, "design_mirror", "Reflect a body about a plane"},
{"pat_curve", "Pattern on Curve", 4, nullptr, nullptr, "Pattern on curve needs a body and a curve", 0x00000090u, 1, 0, false, false, nullptr, nullptr, "Repeat the body along a picked curve"},
{"transform", "Move", 5, "Shift+Y", "key:S+Y", "Transform needs a body — add or import one first", 0x000021feu, 1, 0, false, false, nullptr, "design_move", "Move and/or rotate an existing body"},
{"mate", "Mate", 5, nullptr, "fly:placement#2", "A mate needs two coordinate systems", 0x00001202u, 2, 0, false, false, nullptr, "design_c_coincident", "Assembly: align two CoordSys features (fastened, planar, revolute, slider, cylindrical)"},
{"align", "Align to", 5, nullptr, nullptr, "Align needs a body", 0x00000002u, 1, 0, false, false, nullptr, nullptr, "Align the body to a picked face or plane"},
{"plane", "Plane", 6, "Shift+P", "key:S+P", nullptr, 0x00000453u, 0, 0, false, false, nullptr, "design_plane", "Reference plane (offset / tilt / midplane / tangent / two edges / coincident)"},
{"axis", "Axis", 6, "Shift+A", "key:S+A", nullptr, 0x00000057u, 0, 0, false, false, nullptr, "design_line", "Datum axis (two points, face normal, cylinder centerline, two planes, along edge)"},
{"coordsys_v", "Coord Sys", 6, "Shift+C", "key:S+C", nullptr, 0x00000043u, 0, 0, false, false, nullptr, "design_point", "Datum coordinate system (world point, or face + direction edge)"},
{"helix", "Helix", 6, nullptr, "fly:plane#3", nullptr, 0x00000405u, 0, 0, false, false, nullptr, "design_thread", "Helical curve (spring path) — use as a sweep path for coils / springs / augers"},
{"project", "Project", 6, nullptr, "fly:plane#4", "Project needs a body — add or import one first", 0x00000482u, 1, 0, false, false, nullptr, "design_sketch", "Project body edges onto a plane as sketch entities"},
{"measure", "Measure", 6, nullptr, nullptr, nullptr, 0x000b03feu, 0, 0, false, false, nullptr, nullptr, "Measure between the picked points, edges or faces"},
{"mass_props", "Mass", 6, nullptr, "btn:mass", nullptr, 0x000000feu, 1, 0, false, false, nullptr, "info", "Report the volume and surface area of the selected body"},
{"interference", "Interference", 6, nullptr, nullptr, nullptr, 0x00000200u, 2, 0, false, false, nullptr, nullptr, "Check whether two bodies overlap — reports, changes nothing"},
{"edit_feature", "Edit", 7, nullptr, "btn:edit", nullptr, 0x00007d8eu, 0, 0, false, false, nullptr, "design_edit", "Reopen the selected feature to change what it was made from"},
{"rename", "Rename…", 7, "F2", "btn:rename", "Select a feature, or a body, to rename it", 0x00004080u, 0, 0, false, false, nullptr, nullptr, "Give this feature a name you will recognise in the tree (a body takes its name from the feature that makes it)"},
{"delete_face", "Delete Face", 7, nullptr, "fly:dressup#3", "Delete Face needs a body — add or import one first", 0x0000000eu, 1, 0, false, false, nullptr, "design_delete", "Remove faces from a body and heal the solid"},
{"colour", "Colour", 7, nullptr, "btn:colour", nullptr, 0x000001feu, 1, 0, false, false, nullptr, "color_palette", "Set the selected body's display colour"},
{"delete", "Delete", 7, "Del", "btn:delete", nullptr, 0x000f7c00u, 0, 0, false, false, nullptr, "design_delete", "Delete what is selected"},
{"delete_body", "Delete Body", 7, nullptr, "btn:delete_body", nullptr, 0x000001feu, 1, 0, false, false, nullptr, "design_delete", "Delete this whole body — removes the feature it was made from"},
{"sk_line_t", "Line", 0, "L", "key:L", nullptr, 0x000f8000u, 0, 0, false, true, "Line", "design_line", "Line — click start, then end"},
{"sk_polyline", "Polyline", 0, nullptr, "fly:design_line#1", nullptr, 0x000f8000u, 0, 0, false, true, "Line", "design_polyline", "Click points; click the first point to close the loop, right-click to end it open"},
{"sk_rect", "Corner rectangle", 0, "R", "key:R", nullptr, 0x000f8000u, 0, 0, false, true, "Rectangle", "design_rect", "Rectangle — click two opposite corners"},
{"sk_rect_center", "Centre rectangle", 0, nullptr, "fly:design_rect#1", nullptr, 0x000f8000u, 0, 0, false, true, "Rectangle", "design_crect", "Click center, then a corner"},
{"sk_rect_oblique", "Oblique rectangle", 0, nullptr, "fly:design_rect#2", nullptr, 0x000f8000u, 0, 0, false, true, "Rectangle", "design_rect_oblique", "Click two corners of one edge, then a point for the width"},
{"sk_rect_rounded", "Rounded rectangle", 0, nullptr, "fly:design_rect#3", nullptr, 0x000f8000u, 0, 0, false, true, "Rectangle", "design_rect_rounded", "Click two opposite corners, then a point for the corner radius"},
{"sk_circle", "Centre circle", 0, "C", "key:C", nullptr, 0x000f8000u, 0, 0, false, true, "Circle", "design_circle", "Circle — click center, then radius"},
{"sk_circle_2pt", "2-point circle", 0, nullptr, "fly:design_circle#1", nullptr, 0x000f8000u, 0, 0, false, true, "Circle", "design_circle2pt", "Click two ends of the diameter"},
{"sk_circle_3pt", "3-point circle", 0, nullptr, "fly:design_circle#2", nullptr, 0x000f8000u, 0, 0, false, true, "Circle", "design_circle3pt", "Click three points on the circle"},
{"sk_arc_t", "3-point arc", 0, "A", "key:A", nullptr, 0x000f8000u, 0, 0, false, true, "Arc", "design_arc3pt", "Arc — click start, end, then a point"},
{"sk_arc_tangent", "Tangent arc", 0, nullptr, "fly:design_arc3pt#1", nullptr, 0x000f8000u, 0, 0, false, true, "Arc", "design_tangentarc", "Click start (on the last entity) then end"},
{"sk_arc_center", "Centre-point arc", 0, nullptr, "fly:design_arc3pt#2", nullptr, 0x000f8000u, 0, 0, false, true, "Arc", "design_arc_center", "Click center, then start, then a point for the end angle"},
{"sk_slot", "Slot", 0, "S", "key:S", nullptr, 0x000f8000u, 0, 0, false, true, "Slot", "design_slot", "Slot — two centerline ends, then end radius"},
{"sk_slot_arc", "Arc slot", 0, nullptr, "fly:design_slot#1", nullptr, 0x000f8000u, 0, 0, false, true, "Slot", "design_slot_arc", "Click center, start, end, then a point for the width"},
{"sk_ellipse", "Ellipse", 0, "E", "key:E", nullptr, 0x000f8000u, 0, 0, false, true, "Ellipse", "design_ellipse", "Ellipse — center, major end, minor point"},
{"sk_ellipse_arc", "Elliptical arc", 0, nullptr, "fly:design_ellipse#1", nullptr, 0x000f8000u, 0, 0, false, true, "Ellipse", "design_ellipse_arc", "Click center, major-axis end, minor point, then arc start and end"},
{"sk_spline", "Spline", 0, "B", "key:B", nullptr, 0x000f8000u, 0, 0, false, true, nullptr, "design_bspline", "Spline — click control points"},
{"sk_poly_3", "Triangle", 0, nullptr, "btn:poly#3", nullptr, 0x000f8000u, 0, 0, false, true, "Polygon", "design_polygon", "Triangle — click centre, then a vertex"},
{"sk_poly_4", "Square", 0, nullptr, "btn:poly#4", nullptr, 0x000f8000u, 0, 0, false, true, "Polygon", "design_polygon", "Square — click centre, then a vertex"},
{"sk_poly_5", "Pentagon", 0, nullptr, "btn:poly#5", nullptr, 0x000f8000u, 0, 0, false, true, "Polygon", "design_polygon", "Pentagon — click centre, then a vertex"},
{"sk_polygon", "Hexagon", 0, "G", "btn:poly#6", nullptr, 0x000f8000u, 0, 0, false, true, "Polygon", "design_polygon", "Hexagon — click centre, then a vertex"},
{"sk_poly_8", "Octagon", 0, nullptr, "btn:poly#8", nullptr, 0x000f8000u, 0, 0, false, true, "Polygon", "design_polygon", "Octagon — click centre, then a vertex"},
{"sk_poly_12", "Dodecagon", 0, nullptr, "btn:poly#12", nullptr, 0x000f8000u, 0, 0, false, true, "Polygon", "design_polygon", "Dodecagon — click centre, then a vertex"},
{"sk_poly_inscribed", "Inscribed", 0, nullptr, "btn:polyfit#0", nullptr, 0x000f8000u, 0, 0, false, true, "Polygon", "design_polygon", "Measure the polygon to its corners (inscribed)"},
{"sk_poly_circumscribed", "Circumscribed", 0, nullptr, "btn:polyfit#1", nullptr, 0x000f8000u, 0, 0, false, true, "Polygon", "design_polygon", "Measure the polygon to its flats (circumscribed)"},
{"sk_point_t", "Point", 0, "P", "key:P", nullptr, 0x000f8000u, 0, 0, false, true, nullptr, "design_point", "Point — click to place"},
{"sk_text", "Text", 0, nullptr, "btn:text", nullptr, 0x000f8000u, 0, 0, false, true, nullptr, "design_text", "Type text; its outline is added to this sketch as editable lines"},
{"sk_svg", "SVG", 0, nullptr, "btn:svg", nullptr, 0x000f8000u, 0, 0, false, true, nullptr, "design_svg", "Import an SVG outline into this sketch as editable lines"},
{"sk_offset", "Offset", 1, "O", "key:O", nullptr, 0x000b0000u, 0, 0, false, true, nullptr, "design_offset", "Offset — pick an entity, drag the distance"},
{"sk_trim", "Trim", 2, "T", "key:T", nullptr, 0x000b0000u, 0, 0, false, true, nullptr, "design_trim", "Trim — click a segment to trim it"},
{"sk_fillet", "Fillet", 3, "F", "key:F", nullptr, 0x00090000u, 0, 0, false, true, nullptr, "design_filletedge", "Fillet — pick two lines, set the radius"},
{"sk_chamfer", "Chamfer", 3, "H", "key:H", nullptr, 0x00090000u, 0, 0, false, true, nullptr, "design_chamfer", "Chamfer — pick two lines, set the distance"},
{"sk_array", "Linear array", 4, nullptr, "fly:design_array#0", nullptr, 0x000b0000u, 0, 0, false, true, "Array", "design_array", "Pick entities, drag the spacing handle, click the count; click empty to apply"},
{"sk_array_polar", "Polar array", 4, nullptr, "fly:design_array#1", nullptr, 0x000b0000u, 0, 0, false, true, "Array", "design_polararray", "Pick entities, drag the sweep handle, click the count; click empty to apply"},
{"sk_mirror", "Mirror", 4, "M", "key:M", nullptr, 0x000b0000u, 0, 0, false, true, nullptr, "design_mirror", "Mirror — pick axis, then entities"},
{"sk_move", "Move", 5, nullptr, "fly:design_move#0", nullptr, 0x000f0000u, 0, 0, false, true, "Move", "design_move", "Pick entities, then drag the handle or click the distance; click empty to apply"},
{"sk_rotate", "Rotate", 5, nullptr, "fly:design_move#1", nullptr, 0x000f0000u, 0, 0, false, true, "Move", "design_rotate", "Pick entities, then drag around the pivot or click the angle; click empty to apply"},
{"sk_scale", "Scale", 5, nullptr, "fly:design_move#2", nullptr, 0x000f0000u, 0, 0, false, true, "Move", "design_scale", "Pick entities, then drag the handle or click the factor; click empty to apply"},
{"sk_dimension", "Dimension", 6, "D", "key:D", nullptr, 0x000f8000u, 0, 0, false, true, nullptr, "design_dimension", "Dimension — click 2 points or an entity"},
{"sk_constrain", "Constrain", 6, "K", "key:K", nullptr, 0x000f0000u, 0, 0, false, true, nullptr, "design_constrain", "Constrain the selected sketch entities to each other"},
{"sketch", L("Sketch"), 0, "Shift+S", "key:S+S", L("Click a face or a reference plane in the viewport, then a sketch tool"), 0x00000403u, 0, 0, false, false, nullptr, "design_sketch", L("Click a face or a reference plane, then pick a drawing tool")},
{"extrude", L("Extrude"), 1, "Shift+E", "key:S+E", L("Create a sketch, or pick a solid face, first"), 0x00004002u, 0, 0, false, false, nullptr, "design_extrude", L("Extrude a sketch profile, or push/pull a picked face")},
{"revolve", L("Revolve"), 1, "Shift+R", "key:S+R", L("Create a sketch profile to revolve first"), 0x00004000u, 0, 0, false, false, nullptr, "design_revolve", L("Revolve a profile about an axis")},
{"sweep", L("Sweep"), 1, "Shift+W", "key:S+W", L("Create a profile sketch to sweep first"), 0x00004000u, 0, 2, false, false, nullptr, "design_sweep", L("Sweep a profile along a path")},
{"loft", L("Loft"), 1, "Shift+L", "key:S+L", L("Create at least two profile sketches to loft"), 0x00004000u, 0, 2, false, false, nullptr, "design_loft", L("Loft (skin) between two or more profiles")},
{"thicken", L("Thicken"), 1, nullptr, "fly:material#4", L("Thicken needs a solid body — add or import one first"), 0x0000000au, 1, 0, false, false, nullptr, "design_thicken", L("Offset a solid face into a thin plate (new body)")},
{"rib", L("Rib"), 1, "R", "fly:material#5", L("Rib needs a solid body — add or import one first"), 0x00010000u, 1, 0, false, false, nullptr, "design_rib", L("Grow a thin wall from an open sketch line, fused to a body")},
{"boolean", L("Join"), 1, "Shift+B", "btn:bool#0", L("Boolean needs two bodies — add or import a second one"), 0x00000200u, 2, 0, false, false, nullptr, "design_boolean", L("Fuse the tool body into the target — one solid, no seam")},
{"bool_subtract", L("Subtract"), 1, nullptr, "btn:bool#1", L("Boolean needs two bodies — add or import a second one"), 0x00000200u, 2, 0, false, false, nullptr, "design_boolean", L("Cut the tool body out of the target")},
{"bool_intersect", L("Intersect"), 1, nullptr, "btn:bool#2", L("Boolean needs two bodies — add or import a second one"), 0x00000200u, 2, 0, false, false, nullptr, "design_boolean", L("Keep only where the two bodies overlap")},
{"surf_extrude", L("Surface Extrude"), 1, "Shift+G", "key:S+G", L("Create a sketch first"), 0x00004000u, 0, 0, false, false, nullptr, "design_extrude", L("Extrude a sketch into a sheet body (no end caps)")},
{"surf_revolve", L("Surface Revolve"), 1, nullptr, "fly:surface#1", L("Create a sketch profile to revolve first"), 0x00004000u, 0, 0, false, false, nullptr, "design_revolve", L("Revolve a sketch profile into a sheet body")},
{"surf_loft", L("Surface Loft"), 1, nullptr, "fly:surface#2", L("Create at least two profile sketches to loft"), 0x00004000u, 0, 2, false, false, nullptr, "design_loft", L("Loft (skin) between 2+ profiles, open (no end caps)")},
{"surf_fill", L("Surface Fill"), 1, nullptr, "fly:surface#3", L("Create a closed sketch first"), 0x00004000u, 0, 0, false, false, nullptr, "design_surface", L("Fill a sketch boundary with a smooth face")},
{"thicken_surf", L("Thicken Surface"), 1, nullptr, "fly:surface#5", L("Thicken Surface needs a surface body — make one with a Surface tool first"), 0x00000100u, 0, 0, true, false, nullptr, "design_thicken", L("Thicken a sheet body into a solid")},
{"hole", L("Hole"), 2, "Shift+H", "key:S+H", L("Pick a face or a plane to drill into"), 0x00000402u, 1, 0, false, false, nullptr, "design_hole", L("Drill a hole, centerd on a picked face or placed on a plane")},
{"thread", L("Thread"), 2, "Shift+T", "key:S+T", L("Pick a cylindrical surface (bore / outer) or a circular edge for a thread"), 0x00000024u, 1, 0, false, false, nullptr, "design_thread", L("Thread a cylindrical surface (inner bore / outer) or a circular edge")},
{"shell", L("Shell"), 2, "Shift+K", "key:S+K", L("Shell needs a solid body — add or import one first"), 0x00000082u, 1, 0, false, false, nullptr, "design_shell", L("Hollow the body to a wall thickness, opening a picked face")},
{"cut", L("Cut"), 2, "Shift+X", "key:S+X", L("Cut needs a solid body — add or import one first"), 0x000004feu, 1, 0, false, false, nullptr, "design_cut", L("Trim the body with a plane — drag the offset arrow; keep one half or both")},
{"split", L("Split"), 2, nullptr, nullptr, L("Split needs a solid body — add or import one first"), 0x000000feu, 1, 0, false, false, nullptr, nullptr, L("Split the body along a picked face into two solids")},
{"fillet", L("Fillet"), 3, "Shift+F", "btn:dress#0", L("Pick an edge to round"), 0x000000b2u, 1, 0, false, false, nullptr, "design_filletedge", L("Pick an edge, then drag the radius arrow or type it")},
{"chamfer", L("Chamfer"), 3, nullptr, "btn:dress#1", L("Pick an edge to bevel"), 0x000000b2u, 1, 0, false, false, nullptr, "design_chamfer", L("Pick an edge, then drag the distance arrow or type it")},
{"draft", L("Draft"), 3, "Shift+D", "key:S+D", L("Pick a face to taper"), 0x0000000au, 1, 0, false, false, nullptr, "design_draft", L("Tilt a picked face by a draft angle")},
{"surf_offset", L("Surface Offset"), 3, nullptr, "fly:surface#4", L("Surface Offset needs a surface body — make one with a Surface tool first"), 0x00000100u, 0, 0, true, false, nullptr, "design_offset", L("Offset a sheet body's shell by a signed distance")},
{"pattern", L("Linear pattern"), 4, "Shift+N", "btn:pat#0", L("Pattern needs a solid body — add or import one first"), 0x00006082u, 1, 0, false, false, nullptr, "design_array", L("Repeat the body along a direction — drag the spacing, set the count")},
{"pattern_circular", L("Circular pattern"), 4, nullptr, "btn:pat#1", L("Pattern needs a solid body — add or import one first"), 0x00006082u, 1, 0, false, false, nullptr, "design_polararray", L("Repeat the body around an axis — set the count and sweep")},
{"mirror", L("Mirror"), 4, "Shift+Z", "key:S+Z", L("Mirror needs a body — add or import one first"), 0x000004feu, 1, 0, false, false, nullptr, "design_mirror", L("Reflect a body about a plane")},
{"pat_curve", L("Pattern on Curve"), 4, nullptr, nullptr, L("Pattern on Curve needs a body and a curve"), 0x00000090u, 1, 0, false, false, nullptr, nullptr, L("Repeat the body along a picked curve")},
{"transform", L("Move"), 5, "Shift+Y", "key:S+Y", L("Transform needs a body — add or import one first"), 0x000021feu, 1, 0, false, false, nullptr, "design_move", L("Move and/or rotate an existing body")},
{"mate", L("Mate"), 5, nullptr, "fly:placement#2", L("Mate needs two coordinate systems — create them first"), 0x00001202u, 2, 0, false, false, nullptr, "design_c_coincident", L("Assembly: align two coordinate systems (fastened, planar, revolute, slider, cylindrical)")},
{"align", L("Align to"), 5, nullptr, nullptr, L("Align needs a body — add or import one first"), 0x00000002u, 1, 0, false, false, nullptr, nullptr, L("Align the body to a picked face or plane")},
{"plane", L("Plane"), 6, "Shift+P", "key:S+P", nullptr, 0x00000453u, 0, 0, false, false, nullptr, "design_plane", L("Reference plane (offset / tilt / midplane / tangent / two edges / coincident)")},
{"axis", L("Axis"), 6, "Shift+A", "key:S+A", nullptr, 0x00000057u, 0, 0, false, false, nullptr, "design_line", L("Datum axis (two points, face normal, cylinder centerline, two planes, along edge)")},
{"coordsys_v", L("Coordinate system"), 6, "Shift+C", "key:S+C", nullptr, 0x00000043u, 0, 0, false, false, nullptr, "design_point", L("Datum coordinate system (world point, or face + direction edge)")},
{"helix", L("Helix"), 6, nullptr, "fly:plane#3", nullptr, 0x00000405u, 0, 0, false, false, nullptr, "design_thread", L("Helical curve (spring path) — use as a sweep path for coils / springs / augers")},
{"project", L("Project"), 6, nullptr, "fly:plane#4", L("Project needs a body — add or import one first"), 0x00000482u, 1, 0, false, false, nullptr, "design_sketch", L("Project body edges onto a plane as sketch entities")},
{"measure", L("Measure"), 6, nullptr, nullptr, nullptr, 0x000b03feu, 0, 0, false, false, nullptr, nullptr, L("Measure between the picked points, edges or faces")},
{"mass_props", L("Volume and area"), 6, nullptr, "btn:mass", nullptr, 0x000000feu, 1, 0, false, false, nullptr, "info", L("Report the volume and surface area of the selected body")},
{"interference", L("Interference"), 6, nullptr, "btn:interference", L("Interference needs at least two bodies"), 0x00000280u, 2, 0, false, false, nullptr, nullptr, L("Check whether two bodies overlap — reports, changes nothing")},
{"edit_feature", L("Edit"), 7, nullptr, "btn:edit", nullptr, 0x00007d8eu, 0, 0, false, false, nullptr, "design_edit", L("Reopen the selected feature to change what it was made from")},
{"rename", L("Rename…"), 7, "F2", "btn:rename", L("Select a feature, or a body, to rename it"), 0x00004080u, 0, 0, false, false, nullptr, nullptr, L("Give this feature a name you will recognise in the tree (a body takes its name from the feature that makes it)")},
{"delete_face", L("Delete Face"), 7, nullptr, "fly:dressup#3", L("Delete Face needs a body — add or import one first"), 0x0000000eu, 1, 0, false, false, nullptr, "design_delete", L("Remove faces from a body and heal the solid")},
{"colour", L("Color"), 7, nullptr, "btn:colour", nullptr, 0x000001feu, 1, 0, false, false, nullptr, "color_palette", L("Set the selected body's display color")},
{"delete", L("Delete"), 7, "Del", "btn:delete", nullptr, 0x000f7c00u, 0, 0, false, false, nullptr, "design_delete", L("Delete what is selected")},
{"delete_body", L("Delete Body"), 7, nullptr, "btn:delete_body", nullptr, 0x000001feu, 1, 0, false, false, nullptr, "design_delete", L("Delete this whole body — removes the feature it was made from")},
{"sk_line_t", L("Line"), 0, "L", "key:L", nullptr, 0x000f8000u, 0, 0, false, true, L("Line"), "design_line", L("Line — click start, then end")},
{"sk_polyline", L("Polyline"), 0, nullptr, "fly:design_line#1", nullptr, 0x000f8000u, 0, 0, false, true, L("Line"), "design_polyline", L("Click points; click the first point to close the loop, right-click to end it open")},
{"sk_rect", L("Corner rectangle"), 0, "R", "key:R", nullptr, 0x000f8000u, 0, 0, false, true, L("Rectangle"), "design_rect", L("Rectangle — click two opposite corners")},
{"sk_rect_center", L("Center rectangle"), 0, nullptr, "fly:design_rect#1", nullptr, 0x000f8000u, 0, 0, false, true, L("Rectangle"), "design_crect", L("Click center, then a corner")},
{"sk_rect_oblique", L("Oblique rectangle"), 0, nullptr, "fly:design_rect#2", nullptr, 0x000f8000u, 0, 0, false, true, L("Rectangle"), "design_rect_oblique", L("Click two corners of one edge, then a point for the width")},
{"sk_rect_rounded", L("Rounded rectangle"), 0, nullptr, "fly:design_rect#3", nullptr, 0x000f8000u, 0, 0, false, true, L("Rectangle"), "design_rect_rounded", L("Click two opposite corners, then a point for the corner radius")},
{"sk_circle", L("Center circle"), 0, "C", "key:C", nullptr, 0x000f8000u, 0, 0, false, true, L("Circle"), "design_circle", L("Circle — click center, then radius")},
{"sk_circle_2pt", L("2-point circle"), 0, nullptr, "fly:design_circle#1", nullptr, 0x000f8000u, 0, 0, false, true, L("Circle"), "design_circle2pt", L("Click two ends of the diameter")},
{"sk_circle_3pt", L("3-point circle"), 0, nullptr, "fly:design_circle#2", nullptr, 0x000f8000u, 0, 0, false, true, L("Circle"), "design_circle3pt", L("Click three points on the circle")},
{"sk_arc_t", L("3-point arc"), 0, "A", "key:A", nullptr, 0x000f8000u, 0, 0, false, true, L("Arc"), "design_arc3pt", L("Arc — click start, end, then a point")},
{"sk_arc_tangent", L("Tangent arc"), 0, nullptr, "fly:design_arc3pt#1", nullptr, 0x000f8000u, 0, 0, false, true, L("Arc"), "design_tangentarc", L("Click start (on the last entity) then end")},
{"sk_arc_center", L("Center-point arc"), 0, nullptr, "fly:design_arc3pt#2", nullptr, 0x000f8000u, 0, 0, false, true, L("Arc"), "design_arc_center", L("Click center, then start, then a point for the end angle")},
{"sk_slot", L("Slot"), 0, "S", "key:S", nullptr, 0x000f8000u, 0, 0, false, true, L("Slot"), "design_slot", L("Slot — two centerline ends, then the width")},
{"sk_slot_arc", L("Arc slot"), 0, nullptr, "fly:design_slot#1", nullptr, 0x000f8000u, 0, 0, false, true, L("Slot"), "design_slot_arc", L("Click center, start, end, then a point for the width")},
{"sk_ellipse", L("Ellipse"), 0, "E", "key:E", nullptr, 0x000f8000u, 0, 0, false, true, L("Ellipse"), "design_ellipse", L("Ellipse — center, major end, minor point")},
{"sk_ellipse_arc", L("Elliptical arc"), 0, nullptr, "fly:design_ellipse#1", nullptr, 0x000f8000u, 0, 0, false, true, L("Ellipse"), "design_ellipse_arc", L("Click center, major-axis end, minor point, then arc start and end")},
{"sk_spline", L("Spline"), 0, "B", "key:B", nullptr, 0x000f8000u, 0, 0, false, true, nullptr, "design_bspline", L("Spline — click control points")},
{"sk_poly_3", L("Triangle"), 0, nullptr, "btn:poly#3", nullptr, 0x000f8000u, 0, 0, false, true, L("Polygon"), "design_polygon", L("Triangle — click center, then a vertex")},
{"sk_poly_4", L("Square"), 0, nullptr, "btn:poly#4", nullptr, 0x000f8000u, 0, 0, false, true, L("Polygon"), "design_polygon", L("Square — click center, then a vertex")},
{"sk_poly_5", L("Pentagon"), 0, nullptr, "btn:poly#5", nullptr, 0x000f8000u, 0, 0, false, true, L("Polygon"), "design_polygon", L("Pentagon — click center, then a vertex")},
{"sk_polygon", L("Hexagon"), 0, "G", "btn:poly#6", nullptr, 0x000f8000u, 0, 0, false, true, L("Polygon"), "design_polygon", L("Hexagon — click center, then a vertex")},
{"sk_poly_8", L("Octagon"), 0, nullptr, "btn:poly#8", nullptr, 0x000f8000u, 0, 0, false, true, L("Polygon"), "design_polygon", L("Octagon — click center, then a vertex")},
{"sk_poly_12", L("Dodecagon"), 0, nullptr, "btn:poly#12", nullptr, 0x000f8000u, 0, 0, false, true, L("Polygon"), "design_polygon", L("Dodecagon — click center, then a vertex")},
{"sk_poly_inscribed", L("Inscribed"), 0, nullptr, "btn:polyfit#0", nullptr, 0x000f8000u, 0, 0, false, true, L("Polygon"), "design_polygon", L("Measure the polygon to its corners (inscribed)")},
{"sk_poly_circumscribed", L("Circumscribed"), 0, nullptr, "btn:polyfit#1", nullptr, 0x000f8000u, 0, 0, false, true, L("Polygon"), "design_polygon", L("Measure the polygon to its flats (circumscribed)")},
{"sk_point_t", L("Point"), 0, "P", "key:P", nullptr, 0x000f8000u, 0, 0, false, true, nullptr, "design_point", L("Point — click to place")},
{"sk_text", L("Text"), 0, nullptr, "btn:text", nullptr, 0x000f8000u, 0, 0, false, true, nullptr, "design_text", L("Type text; its outline is added to this sketch as editable lines")},
{"sk_svg", L("SVG"), 0, nullptr, "btn:svg", nullptr, 0x000f8000u, 0, 0, false, true, nullptr, "design_svg", L("Import an SVG outline into this sketch as editable lines")},
{"sk_offset", L("Offset"), 1, "O", "key:O", nullptr, 0x000b0000u, 0, 0, false, true, nullptr, "design_offset", L("Offset — pick an entity, drag the distance")},
{"sk_trim", L("Trim"), 2, "T", "key:T", nullptr, 0x000b0000u, 0, 0, false, true, nullptr, "design_trim", L("Trim — click a segment to trim it")},
{"sk_fillet", L("Fillet"), 3, "F", "key:F", nullptr, 0x00090000u, 0, 0, false, true, nullptr, "design_filletedge", L("Fillet — pick two lines, set the radius")},
{"sk_chamfer", L("Chamfer"), 3, "H", "key:H", nullptr, 0x00090000u, 0, 0, false, true, nullptr, "design_chamfer", L("Chamfer — pick two lines, set the distance")},
{"sk_array", L("Linear array"), 4, nullptr, "fly:design_array#0", nullptr, 0x000b0000u, 0, 0, false, true, L("Array"), "design_array", L("Pick entities, drag the spacing handle, click the count; click empty to apply")},
{"sk_array_polar", L("Polar array"), 4, nullptr, "fly:design_array#1", nullptr, 0x000b0000u, 0, 0, false, true, L("Array"), "design_polararray", L("Pick entities, drag the sweep handle, click the count; click empty to apply")},
{"sk_mirror", L("Mirror"), 4, "M", "key:M", nullptr, 0x000b0000u, 0, 0, false, true, nullptr, "design_mirror", L("Mirror — pick axis, then entities")},
{"sk_move", L("Move"), 5, nullptr, "fly:design_move#0", nullptr, 0x000f0000u, 0, 0, false, true, L("Move"), "design_move", L("Pick entities, then drag the handle or click the distance; click empty to apply")},
{"sk_rotate", L("Rotate"), 5, nullptr, "fly:design_move#1", nullptr, 0x000f0000u, 0, 0, false, true, L("Move"), "design_rotate", L("Pick entities, then drag around the pivot or click the angle; click empty to apply")},
{"sk_scale", L("Scale"), 5, nullptr, "fly:design_move#2", nullptr, 0x000f0000u, 0, 0, false, true, L("Move"), "design_scale", L("Pick entities, then drag the handle or click the factor; click empty to apply")},
{"sk_dimension", L("Dimension"), 6, "D", "key:D", nullptr, 0x000f8000u, 0, 0, false, true, nullptr, "design_dimension", L("Dimension — click 2 points or an entity")},
{"sk_constrain", L("Constrain"), 6, "K", "key:K", nullptr, 0x000f0000u, 0, 0, false, true, nullptr, "design_constrain", L("Constrain the selected sketch entities to each other")},
// Same verb, model-mode vocabulary: offered when a SKETCH is selected (bit 14, SkLoop), the
// state a user is in right after finishing one. Without this row the only way in was the
// toolbar icon, and constraints read as absent — see the Onshape-comparison report.
{"constrain", "Constrain sketch", 7, nullptr, "btn:constrain", "Select a sketch to constrain it", 0x00004000u, 0, 1, false, false, nullptr, "design_constrain", "Add dimensions and relations (coincident, tangent, parallel...) to the selected sketch"},
{"sk_construct", "Construction", 6, "Q", "key:Q", nullptr, 0x000b8000u, 0, 0, false, true, nullptr, nullptr, "Toggle construction: geometry that guides but is never built"},
{"sk_extend", "Extend", 7, "X", "key:X", nullptr, 0x000b0000u, 0, 0, false, true, nullptr, "design_extend", "Extend — click a line/arc to extend it"},
{"sk_delete", "Delete", 7, "Del", "btn:sk_delete", nullptr, 0x000f0000u, 0, 0, false, true, nullptr, "design_delete", "Delete the selected sketch entities"},
// It sits in the Reference row, the address the sketch-mode Constrain has: one verb, one row.
{"constrain", L("Constrain sketch"), 6, nullptr, "btn:constrain", L("Select a sketch to constrain it"), 0x00004000u, 0, 1, false, false, nullptr, "design_constrain", L("Add dimensions and relations (coincident, tangent, parallel...) to the selected sketch")},
{"sk_construct", L("Construction"), 6, "Q", "key:Q", nullptr, 0x000b8000u, 0, 0, false, true, nullptr, nullptr, L("Toggle construction: geometry that guides but is never built")},
{"sk_extend", L("Extend"), 7, "X", "key:X", nullptr, 0x000b0000u, 0, 0, false, true, nullptr, "design_extend", L("Extend — click a line/arc to extend it")},
{"sk_delete", L("Delete"), 7, "Del", "btn:sk_delete", nullptr, 0x000f0000u, 0, 0, false, true, nullptr, "design_delete", L("Delete the selected sketch entities")},
// Typing the defining number of the element you pointed at. Three rows rather than one so
// each names the quantity in the drawing-office word for THAT element; all three land on
// the same handler, because dimension_kind() already resolves the quantity from the
// selection. Without these, an element's own numbers were reachable only by arming the
// Dimension tool and re-picking geometry that was already selected.
{"sk_length", "Length…", 7, "V", "key:V", nullptr, 0x00010000u, 0, 0, false, true, nullptr, "design_dimension", "Type the length of this line"},
{"sk_radius", "Radius / diameter…", 7, "V", "key:V", nullptr, 0x00020000u, 0, 0, false, true, nullptr, "design_dimension", "Type the radius of this arc, or the diameter of this circle"},
{"sk_angdist", "Angle / distance…", 7, "V", "key:V", nullptr, 0x00080000u, 0, 0, false, true, nullptr, "design_dimension", "Type the angle between two lines, or the distance between the two picks"},
{"sk_length", L("Length…"), 7, "V", "key:V", nullptr, 0x00010000u, 0, 0, false, true, nullptr, "design_dimension", L("Type the length of this line")},
{"sk_radius", L("Radius / diameter…"), 7, "V", "key:V", nullptr, 0x00020000u, 0, 0, false, true, nullptr, "design_dimension", L("Type the radius of this arc, or the diameter of this circle")},
{"sk_angdist", L("Angle / distance…"), 7, "V", "key:V", nullptr, 0x00080000u, 0, 0, false, true, nullptr, "design_dimension", L("Type the angle between two lines, or the distance between the two picks")},
};
static const int kOfferVerbCount = 92;
File diff suppressed because it is too large Load Diff
+2
View File
@@ -128,6 +128,7 @@ private:
void on_shape_changed();
void on_add_sketch();
std::string feature_name(const wxString& kind) const;
void on_add_extrude();
void on_add_dressup();
void on_add_hole();
@@ -528,6 +529,7 @@ private:
wxStaticText* m_extrude_sketch_label{nullptr};
int m_extrude_sketch_ref{-1};
int m_extrude_auto_body{-1}; // body the profile touches: the inferred Join target
// Revolve controls (sweep a sketch profile about an in-plane axis).
wxStaticText* m_revolve_sketch_label{nullptr};
+2 -14
View File
@@ -65,19 +65,9 @@ static double ray_segment_dist3(const Vec3d& ro, const Vec3d& rd, const Vec3d& a
return wxPoint(int(sx + 0.5), int(sy + 0.5));
}
// The kernel's weld tolerance follows the app preference, and it must be pushed at EVERY
// point that starts a sketch session: a Constrain session never passes through begin(), and
// it uses region_loops()/connected_loop(), which read the same tolerance. Pushing in one
// place only would leave those sessions on whatever the previous session set.
static void push_auto_close_pref()
{
Slic3r::set_sketch_auto_close(wxGetApp().is_auto_close_sketch_loops());
}
void DesignSketchTool::begin(const SketchPlane& plane, Mode mode)
{
m_sketch_redo.clear();
push_auto_close_pref();
m_plane = plane;
m_mode = mode;
@@ -2451,7 +2441,6 @@ void DesignSketchTool::finish()
void DesignSketchTool::begin_constrain(const SketchProfile& prof, const SketchPlane& plane)
{
push_auto_close_pref();
m_plane = plane;
m_mode = Mode::Constrain;
m_points = prof.points;
@@ -2466,7 +2455,6 @@ void DesignSketchTool::begin_constrain(const SketchProfile& prof, const SketchPl
void DesignSketchTool::begin_constrain_entities(const std::vector<SketchEntity>& ents,
const SketchPlane& plane)
{
push_auto_close_pref();
m_plane = plane;
m_mode = Mode::Constrain;
m_constrain_entities = true;
@@ -10422,7 +10410,7 @@ bool DesignSketchTool::on_mouse_impl(wxMouseEvent& evt, GLCanvas3D& canvas)
// In-canvas edit-op tools (Fillet/Chamfer/Offset/Mirror): pick entities, then a
// draggable arrow + editable value label (Mirror: a two-phase pick) drives a live
// ghost. A click on empty space confirms; right-click/Esc cancels the gesture.
// ghost. Enter or ✓ applies, Esc or right-click discards (charter 4.2).
// Standalone Trim / Extend scissors: click a segment to cut it back to (Trim) or out to
// (Extend) its nearest intersection with the other live entities. One cut per click; the
// tool stays active for more cuts; right-click exits. Drag falls through so the camera can
@@ -10502,7 +10490,7 @@ bool DesignSketchTool::on_mouse_impl(wxMouseEvent& evt, GLCanvas3D& canvas)
// In-canvas transform tools (Move/Rotate/Scale/Array/PolarArray): pick subject
// entities, then a single draggable handle + editable value label(s) drive a live
// ghost. A click on empty space confirms; right-click drops the gesture / exits.
// ghost. Enter or ✓ applies, Esc or right-click discards the pending transform.
if (is_transform_mode()) {
if (evt.Moving()) {
screen_to_plane(canvas, evt, m_cursor);
+35 -27
View File
@@ -2,6 +2,7 @@
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/ImGuiWrapper.hpp"
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/GUI/GUI_ObjectList.hpp"
#include "slic3r/GUI/NotificationManager.hpp"
@@ -20,7 +21,11 @@ GLGizmoPrimitive::GLGizmoPrimitive(GLCanvas3D& parent, const std::string& icon_f
bool GLGizmoPrimitive::on_init() { return true; }
std::string GLGizmoPrimitive::on_get_name() const { return _u8L("Primitive"); }
bool GLGizmoPrimitive::on_is_activable() const { return true; }
// Part of the experimental CAD feature: built into every CAD build, but offered in the Prepare
// toolbar only when that feature is switched on in Preferences — switched off, Prepare must be
// exactly what it is without it.
bool GLGizmoPrimitive::on_is_activable() const { return wxGetApp().is_enable_cad_feature(); }
bool GLGizmoPrimitive::on_is_selectable() const { return wxGetApp().is_enable_cad_feature(); }
void GLGizmoPrimitive::on_render() {}
void GLGizmoPrimitive::on_set_state()
{ if (m_state == EState::On) { m_params = PrimitiveParams{}; m_preview_dirty = true; } }
@@ -80,14 +85,15 @@ void GLGizmoPrimitive::on_render_input_window(float x, float y, float bottom_lim
| ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse
| ImGuiWindowFlags_NoTitleBar);
if (ImGui::CollapsingHeader("Shape", ImGuiTreeNodeFlags_DefaultOpen)) {
static const char* names[] = {"Box", "Cylinder", "Sphere", "Cone", "Torus"};
if (ImGui::CollapsingHeader(_u8L("Shape").c_str(), ImGuiTreeNodeFlags_DefaultOpen)) {
const std::string names_s[] = {_u8L("Box"), _u8L("Cylinder"), _u8L("Sphere"), _u8L("Cone"), _u8L("Torus")};
const char* names[] = {names_s[0].c_str(), names_s[1].c_str(), names_s[2].c_str(), names_s[3].c_str(), names_s[4].c_str()};
int cur = (int)m_params.type;
if (ImGui::Combo("##type", &cur, names, (int)PrimitiveType::COUNT)) {
m_params.type = (PrimitiveType)cur;
m_preview_dirty = true;
}
ImGui::Text("Quick:");
ImGui::TextUnformatted(_u8L("Quick:").c_str());
ImGui::SameLine();
if (ImGui::SmallButton("10mm")) apply_preset("10mm cube", 10, 10, 10);
ImGui::SameLine();
@@ -98,7 +104,7 @@ void GLGizmoPrimitive::on_render_input_window(float x, float y, float bottom_lim
ImGui::Separator();
if (ImGui::CollapsingHeader("Dimensions", ImGuiTreeNodeFlags_DefaultOpen)) {
if (ImGui::CollapsingHeader(_u8L("Dimensions").c_str(), ImGuiTreeNodeFlags_DefaultOpen)) {
auto dim = [&](const char* label, double& val, double step=0.5, double fast=5.0) {
ImGui::SetNextItemWidth(130);
if (ImGui::InputDouble(label, &val, step, fast, "%.1f mm")) m_preview_dirty = true;
@@ -106,25 +112,25 @@ void GLGizmoPrimitive::on_render_input_window(float x, float y, float bottom_lim
};
switch (m_params.type) {
case PrimitiveType::Box:
dim("Width (X)", m_params.box_w);
dim("Depth (Y)", m_params.box_d);
dim("Height (Z)", m_params.box_h);
dim(_u8L("Width (X)").c_str(), m_params.box_w);
dim(_u8L("Depth (Y)").c_str(), m_params.box_d);
dim(_u8L("Height (Z)").c_str(), m_params.box_h);
break;
case PrimitiveType::Cylinder:
dim("Radius", m_params.cyl_radius);
dim("Height", m_params.cyl_height);
dim(_u8L("Radius").c_str(), m_params.cyl_radius);
dim(_u8L("Height").c_str(), m_params.cyl_height);
break;
case PrimitiveType::Sphere:
dim("Radius", m_params.sph_radius);
dim(_u8L("Radius").c_str(), m_params.sph_radius);
break;
case PrimitiveType::Cone:
dim("Bottom R", m_params.cone_r1);
dim("Top R", m_params.cone_r2);
dim("Height", m_params.cone_height);
dim(_u8L("Bottom R").c_str(), m_params.cone_r1);
dim(_u8L("Top R").c_str(), m_params.cone_r2);
dim(_u8L("Height").c_str(), m_params.cone_height);
break;
case PrimitiveType::Torus:
dim("Major R", m_params.torus_r1);
dim("Minor R", m_params.torus_r2, 0.1, 1.0);
dim(_u8L("Major R").c_str(), m_params.torus_r1);
dim(_u8L("Minor R").c_str(), m_params.torus_r2, 0.1, 1.0);
break;
default: break;
}
@@ -132,26 +138,28 @@ void GLGizmoPrimitive::on_render_input_window(float x, float y, float bottom_lim
ImGui::Separator();
if (ImGui::CollapsingHeader("Fillet / Chamfer")) {
ImGui::Checkbox("Enable", &m_params.dressup_enabled);
if (ImGui::CollapsingHeader(_u8L("Fillet / Chamfer").c_str())) {
ImGui::Checkbox(_u8L("Enable").c_str(), &m_params.dressup_enabled);
if (m_params.dressup_enabled) {
static const char* dn[] = {"Fillet", "Chamfer"};
const std::string dn_s[] = {_u8L("Fillet"), _u8L("Chamfer")};
const char* dn[] = {dn_s[0].c_str(), dn_s[1].c_str()};
int du = (int)m_params.dressup_type;
ImGui::SetNextItemWidth(100);
if (ImGui::Combo("##dtype", &du, dn, 2)) { m_params.dressup_type = (DressUpType)du; m_preview_dirty = true; }
static const char* fn[] = {"All edges", "Top edges", "Bottom edges", "Lateral edges"};
const std::string fn_s[] = {_u8L("All edges"), _u8L("Top edges"), _u8L("Bottom edges"), _u8L("Lateral edges")};
const char* fn[] = {fn_s[0].c_str(), fn_s[1].c_str(), fn_s[2].c_str(), fn_s[3].c_str()};
int fg = (int)m_params.dressup_faces;
ImGui::SetNextItemWidth(140);
if (ImGui::Combo("Edges", &fg, fn, 4)) { m_params.dressup_faces = (FaceGroup)fg; m_preview_dirty = true; }
if (ImGui::Combo(_u8L("Edges").c_str(), &fg, fn, 4)) { m_params.dressup_faces = (FaceGroup)fg; m_preview_dirty = true; }
if (m_params.dressup_type == DressUpType::Fillet) {
ImGui::SetNextItemWidth(100);
if (ImGui::InputDouble("Radius", &m_params.dressup_radius, 0.1, 1.0, "%.1f mm")) {
if (ImGui::InputDouble(_u8L("Radius").c_str(), &m_params.dressup_radius, 0.1, 1.0, "%.1f mm")) {
if (m_params.dressup_radius < 0.1) m_params.dressup_radius = 0.1;
m_preview_dirty = true;
}
} else {
ImGui::SetNextItemWidth(100);
if (ImGui::InputDouble("Distance", &m_params.dressup_chamfer_dist, 0.1, 1.0, "%.1f mm")) {
if (ImGui::InputDouble(_u8L("Distance").c_str(), &m_params.dressup_chamfer_dist, 0.1, 1.0, "%.1f mm")) {
if (m_params.dressup_chamfer_dist < 0.1) m_params.dressup_chamfer_dist = 0.1;
m_preview_dirty = true;
}
@@ -161,9 +169,9 @@ void GLGizmoPrimitive::on_render_input_window(float x, float y, float bottom_lim
ImGui::Separator();
if (ImGui::CollapsingHeader("Quality")) {
if (ImGui::CollapsingHeader(_u8L("Quality").c_str())) {
ImGui::SetNextItemWidth(130);
if (ImGui::InputDouble("Mesh resolution", &m_params.linear_deflection, 0.001, 0.1, "%.3f mm")) {
if (ImGui::InputDouble(_u8L("Mesh resolution").c_str(), &m_params.linear_deflection, 0.001, 0.1, "%.3f mm")) {
if (m_params.linear_deflection < 0.001) m_params.linear_deflection = 0.001;
if (m_params.linear_deflection > 1.0) m_params.linear_deflection = 1.0;
m_preview_dirty = true;
@@ -172,10 +180,10 @@ void GLGizmoPrimitive::on_render_input_window(float x, float y, float bottom_lim
ImGui::Separator();
if (ImGui::Button("Add Shape", {-1, 28}))
if (ImGui::Button(_u8L("Add Shape").c_str(), {-1, 28}))
apply_primitive();
if (ImGui::Button("Close", {-1, 0}))
if (ImGui::Button(_u8L("Close").c_str(), {-1, 0}))
m_parent.reset_all_gizmos();
GizmoImguiEnd();
@@ -20,6 +20,7 @@ protected:
bool on_init() override;
std::string on_get_name() const override;
bool on_is_activable() const override;
bool on_is_selectable() const override;
void on_render() override;
void on_set_state() override;
CommonGizmosDataID on_get_requirements() const override;
+46 -37
View File
@@ -2,6 +2,7 @@
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/ImGuiWrapper.hpp"
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/GUI/GUI_ObjectList.hpp"
#include "slic3r/GUI/NotificationManager.hpp"
@@ -15,8 +16,6 @@
#endif
#include <imgui/imgui_internal.h>
#define UL(s) Slic3r::GUI::I18N::translate_utf8((s)).c_str()
namespace Slic3r {
namespace GUI {
@@ -25,7 +24,11 @@ GLGizmoSketch::GLGizmoSketch(GLCanvas3D& parent, const std::string& icon_filenam
bool GLGizmoSketch::on_init() { return true; }
std::string GLGizmoSketch::on_get_name() const { return _u8L("Sketch"); }
bool GLGizmoSketch::on_is_activable() const { return true; }
// Part of the experimental CAD feature: built into every CAD build, but offered in the Prepare
// toolbar only when that feature is switched on in Preferences — switched off, Prepare must be
// exactly what it is without it.
bool GLGizmoSketch::on_is_activable() const { return wxGetApp().is_enable_cad_feature(); }
bool GLGizmoSketch::on_is_selectable() const { return wxGetApp().is_enable_cad_feature(); }
void GLGizmoSketch::on_render() {}
void GLGizmoSketch::on_set_state() { if (m_state == EState::On) clear_all(); }
bool GLGizmoSketch::on_mouse(const wxMouseEvent&) { return false; }
@@ -257,8 +260,9 @@ void GLGizmoSketch::on_render_input_window(float x, float y, float bottom_limit)
| ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse
| ImGuiWindowFlags_NoTitleBar);
if (ImGui::CollapsingHeader(UL("Profile"), ImGuiTreeNodeFlags_DefaultOpen)) {
static const char* names[] = {"Line", "Rectangle", "Circle", "Polygon"};
if (ImGui::CollapsingHeader(_u8L("Profile").c_str(), ImGuiTreeNodeFlags_DefaultOpen)) {
const std::string names_s[] = {_u8L("Line"), _u8L("Rectangle"), _u8L("Circle"), _u8L("Polygon")};
const char* names[] = {names_s[0].c_str(), names_s[1].c_str(), names_s[2].c_str(), names_s[3].c_str()};
int cur = (int)m_tool;
if (ImGui::Combo("##shape", &cur, names, (int)SketchTool::COUNT)) {
m_tool = (SketchTool)cur;
@@ -266,39 +270,40 @@ void GLGizmoSketch::on_render_input_window(float x, float y, float bottom_limit)
}
ImGui::SameLine();
if (m_imgui->button("+##newprofile")) m_active_profile = -1;
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", UL("Start new profile (for holes)"));
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", _u8L("Start new profile (for holes)").c_str());
if (m_tool == SketchTool::Rectangle) {
ImGui::SetNextItemWidth(80); if (ImGui::InputDouble("W", &m_rect_w,1,10,"%.0f")) build_preset_profile();
ImGui::SetNextItemWidth(80); if (ImGui::InputDouble(_u8L("Width").c_str(), &m_rect_w,1,10,"%.0f")) build_preset_profile();
ImGui::SameLine();
ImGui::SetNextItemWidth(80); if (ImGui::InputDouble("H", &m_rect_h,1,10,"%.0f")) build_preset_profile();
ImGui::SetNextItemWidth(80); if (ImGui::InputDouble(_u8L("Height").c_str(), &m_rect_h,1,10,"%.0f")) build_preset_profile();
} else if (m_tool == SketchTool::Circle) {
ImGui::SetNextItemWidth(80); if (ImGui::InputDouble("R", &m_circle_r,1,5,"%.0f")) build_preset_profile();
ImGui::SetNextItemWidth(80); if (ImGui::InputDouble(_u8L("Radius").c_str(), &m_circle_r,1,5,"%.0f")) build_preset_profile();
ImGui::SameLine();
ImGui::SetNextItemWidth(80); if (ImGui::SliderInt("Seg", &m_circle_seg,8,64)) build_preset_profile();
ImGui::SetNextItemWidth(80); if (ImGui::SliderInt(_u8L("Segments").c_str(), &m_circle_seg,8,64)) build_preset_profile();
} else if (m_tool == SketchTool::Polygon) {
ImGui::SetNextItemWidth(80); if (ImGui::SliderInt("Sides", &m_poly_sides,3,12)) build_preset_profile();
ImGui::SetNextItemWidth(80); if (ImGui::SliderInt(_u8L("Sides").c_str(), &m_poly_sides,3,12)) build_preset_profile();
ImGui::SameLine();
ImGui::SetNextItemWidth(80); if (ImGui::InputDouble("R", &m_poly_r,1,5,"%.0f")) build_preset_profile();
ImGui::SetNextItemWidth(80); if (ImGui::InputDouble((_u8L("Radius") + "##poly").c_str(), &m_poly_r,1,5,"%.0f")) build_preset_profile();
} else {
ImGui::Text("%s", UL("Click on canvas to draw"));
ImGui::Text("%s", _u8L("Click on canvas to draw").c_str());
}
ImGui::Checkbox(UL("Snap to grid"), &m_snap_grid);
ImGui::Checkbox(_u8L("Snap to grid").c_str(), &m_snap_grid);
ImGui::SameLine();
ImGui::SetNextItemWidth(80); ImGui::InputFloat("Step", &m_grid_step, 1, 5, "%.0f mm");
ImGui::SetNextItemWidth(80); ImGui::InputFloat(_u8L("Grid step").c_str(), &m_grid_step, 1, 5, "%.0f mm");
draw_canvas();
if (!m_profiles.empty()) {
ImGui::Text("%s: %zu", UL("Profiles"), m_profiles.size());
ImGui::Text("%s: %zu", _u8L("Profiles").c_str(), m_profiles.size());
for (int i = 0; i < (int)m_profiles.size(); ++i) {
auto& p = m_profiles[i];
ImGui::PushID(i);
bool outer = (i == 0);
ImVec4 col = outer ? ImVec4(0,1,0,1) : ImVec4(1,0.3f,0.3f,1);
const char* label = outer ? "Outer" : "Hole";
ImGui::TextColored(col, "%s %d: %zu pts %s", label, i+1, p.points.size(), p.closed ? "CLOSED" : "");
const std::string label = outer ? _u8L("Outer") : _u8L("Hole");
ImGui::TextColored(col, "%s %d: %zu %s %s", label.c_str(), i+1, p.points.size(),
_u8L("points").c_str(), p.closed ? _u8L("closed").c_str() : "");
ImGui::SameLine();
if (ImGui::SmallButton("X")) delete_profile(i);
ImGui::PopID();
@@ -311,59 +316,63 @@ void GLGizmoSketch::on_render_input_window(float x, float y, float bottom_limit)
bool is_revolve = false;
bool has_sel = false;
if (ImGui::CollapsingHeader(UL("Operation"), ImGuiTreeNodeFlags_DefaultOpen)) {
if (ImGui::CollapsingHeader(_u8L("Operation").c_str(), ImGuiTreeNodeFlags_DefaultOpen)) {
static int pi = 0;
if (ImGui::Combo(UL("Plane"), &pi, "XY (Top)\0XZ (Front)\0YZ (Side)\0"))
const std::string planes = "XY (" + _u8L("Top") + ")" + std::string(1, '\0') + "XZ (" + _u8L("Front") + ")"
+ std::string(1, '\0') + "YZ (" + _u8L("Side") + ")" + std::string(2, '\0');
if (ImGui::Combo(_u8L("Plane").c_str(), &pi, planes.c_str()))
m_plane = (pi==0) ? SketchPlane::XY() : (pi==1) ? SketchPlane::XZ() : SketchPlane::YZ();
is_revolve = (m_sp.revolve_deg > 0 && m_sp.revolve_deg < 360);
ImGui::SetNextItemWidth(100);
if (ImGui::InputDouble(UL("Revolve deg"), &m_sp.revolve_deg, 15, 90, "%.0f")) {
if (ImGui::InputDouble(_u8L("Revolve deg").c_str(), &m_sp.revolve_deg, 15, 90, "%.0f")) {
if (m_sp.revolve_deg > 360) m_sp.revolve_deg = 360;
if (m_sp.revolve_deg < 0) m_sp.revolve_deg = 0;
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", UL("Set to 0 for extrude, >0 for revolve"));
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", _u8L("Set to 0 for extrude, >0 for revolve").c_str());
if (!is_revolve) {
ImGui::SetNextItemWidth(100);
ImGui::InputDouble(UL("Length"), &m_sp.extrude_len, 0.5, 5, "%.1f mm");
ImGui::InputDouble(_u8L("Length").c_str(), &m_sp.extrude_len, 0.5, 5, "%.1f mm");
ImGui::SameLine();
ImGui::Checkbox(UL("Symmetric"), &m_sp.extrude_sym);
ImGui::Checkbox(_u8L("Symmetric").c_str(), &m_sp.extrude_sym);
}
has_sel = !m_parent.get_selection().is_empty();
if (has_sel) {
if (ImGui::Checkbox(UL("Pocket (cut)"), &m_sp.is_pocket))
if (ImGui::Checkbox(_u8L("Pocket (cut)").c_str(), &m_sp.is_pocket))
if (m_sp.is_pocket) m_sp.dressup_enabled = false;
} else m_sp.is_pocket = false;
}
ImGui::Separator();
if (!m_sp.is_pocket && ImGui::CollapsingHeader(UL("Fillet / Chamfer"))) {
ImGui::Checkbox(UL("Enable"), &m_sp.dressup_enabled);
if (!m_sp.is_pocket && ImGui::CollapsingHeader(_u8L("Fillet / Chamfer").c_str())) {
ImGui::Checkbox(_u8L("Enable").c_str(), &m_sp.dressup_enabled);
if (m_sp.dressup_enabled) {
static const char* dn[] = {"Fillet", "Chamfer"};
const std::string dn_s[] = {_u8L("Fillet"), _u8L("Chamfer")};
const char* dn[] = {dn_s[0].c_str(), dn_s[1].c_str()};
int du = (int)m_sp.dressup_type;
ImGui::SetNextItemWidth(100);
if (ImGui::Combo("##dtype", &du, dn, 2)) m_sp.dressup_type = (DressUpType)du;
static const char* fn[] = {"All edges", "Top edges", "Bottom edges", "Lateral edges"};
const std::string fn_s[] = {_u8L("All edges"), _u8L("Top edges"), _u8L("Bottom edges"), _u8L("Lateral edges")};
const char* fn[] = {fn_s[0].c_str(), fn_s[1].c_str(), fn_s[2].c_str(), fn_s[3].c_str()};
int fg = (int)m_sp.dressup_faces;
ImGui::SetNextItemWidth(140);
ImGui::Combo(UL("Edges"), &fg, fn, 4); m_sp.dressup_faces = (FaceGroup)fg;
ImGui::Combo(_u8L("Edges").c_str(), &fg, fn, 4); m_sp.dressup_faces = (FaceGroup)fg;
ImGui::SetNextItemWidth(100);
if (m_sp.dressup_type == DressUpType::Fillet)
ImGui::InputDouble(UL("Radius"), &m_sp.dressup_radius, 0.1, 1, "%.1f mm");
ImGui::InputDouble(_u8L("Radius").c_str(), &m_sp.dressup_radius, 0.1, 1, "%.1f mm");
else
ImGui::InputDouble(UL("Distance"), &m_sp.dressup_chamfer_dist, 0.1, 1, "%.1f mm");
ImGui::InputDouble(_u8L("Distance").c_str(), &m_sp.dressup_chamfer_dist, 0.1, 1, "%.1f mm");
}
}
ImGui::Separator();
bool ok = has_closed_profile();
if (ok) ImGui::TextColored({0,1,0,1}, "%zu %s", m_profiles.size(), UL("closed profile(s)"));
else ImGui::TextColored({0.6f,0.6f,0.6f,1}, "%s", UL("Draw a closed profile to enable"));
if (ok) ImGui::TextColored({0,1,0,1}, "%zu %s", m_profiles.size(), _u8L("closed profile(s)").c_str());
else ImGui::TextColored({0.6f,0.6f,0.6f,1}, "%s", _u8L("Draw a closed profile to enable").c_str());
auto btn = [&](const char* label, bool enabled) {
if (!enabled) { ImGui::PushItemFlag(ImGuiItemFlags_Disabled,true); ImGui::PushStyleColor(ImGuiCol_Button,{0.25f,0.25f,0.25f,1}); }
@@ -373,14 +382,14 @@ void GLGizmoSketch::on_render_input_window(float x, float y, float bottom_limit)
};
if (m_sp.is_pocket && has_sel) {
if (btn(_u8L("Pocket (Cut)").c_str(), ok)) apply_pocket();
if (btn(_u8L("Pocket (cut)").c_str(), ok)) apply_pocket();
} else if (is_revolve) {
if (btn(_u8L("Revolve").c_str(), ok)) apply_revolve();
} else {
if (btn(_u8L("Extrude").c_str(), ok)) apply_extrude();
}
if (ImGui::Button(_u8L("Clear All").c_str(), {-1,0})) clear_all();
if (ImGui::Button(_u8L("Clear all").c_str(), {-1,0})) clear_all();
if (ImGui::Button(_u8L("Close").c_str(), {-1,0})) m_parent.reset_all_gizmos();
GizmoImguiEnd();
@@ -448,7 +457,7 @@ void GLGizmoSketch::apply_pocket()
mo->ensure_on_bed();
wxGetApp().plater()->update();
clear_all();
wxGetApp().notification_manager()->push_notification(NotificationType::CustomNotification, NotificationManager::NotificationLevel::RegularNotificationLevel, UL("Pocket added (negative volume)"));
wxGetApp().notification_manager()->push_notification(NotificationType::CustomNotification, NotificationManager::NotificationLevel::RegularNotificationLevel, _u8L("Pocket added (negative volume)").c_str());
} catch (const std::exception& e) {
wxGetApp().notification_manager()->push_notification(NotificationType::CustomNotification, NotificationManager::NotificationLevel::ErrorNotificationLevel, std::string("Pocket: ")+e.what());
}
+1
View File
@@ -22,6 +22,7 @@ protected:
bool on_init() override;
std::string on_get_name() const override;
bool on_is_activable() const override;
bool on_is_selectable() const override;
void on_render() override;
void on_set_state() override;
CommonGizmosDataID on_get_requirements() const override;
+15 -2
View File
@@ -10074,10 +10074,23 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
// a user sees after opening a design they spent an hour on — it reads as "your work is gone"
// when the recipe has in fact just been loaded and the Design tab will rehydrate it. Count a
// recipe that came from THESE files as geometry.
if (tolal_model_count <= 0 && !loaded_cad_recipe && !q->m_exported_file) {
//
// Only where the Design tab can actually show it, though. With the CAD feature switched off
// (or not built) the recipe is still carried through to the next save, but nothing will
// display it, so an empty plate needs saying — and saying why.
bool cad_can_show = false;
#ifdef SLIC3R_CAD
cad_can_show = wxGetApp().is_enable_cad_feature();
#endif
if (tolal_model_count <= 0 && !q->m_exported_file && (!loaded_cad_recipe || !cad_can_show)) {
dlg.Hide();
if (!is_user_cancel) {
MessageDialog msg(wxGetApp().mainframe, _L("The file does not contain any geometry data."), _L("Warning"), wxYES | wxICON_WARNING);
const wxString text = loaded_cad_recipe
? _L("This project contains a model made in the Design tab and no other geometry. "
"Enable \"CAD feature (experimental)\" in Preferences and restart to see and edit it; "
"it is kept when the project is saved.")
: _L("The file does not contain any geometry data.");
MessageDialog msg(wxGetApp().mainframe, text, _L("Warning"), wxYES | wxICON_WARNING);
if (msg.ShowModal() == wxID_YES) {}
}
}
+11 -12
View File
@@ -9,7 +9,6 @@
#include "I18N.hpp"
#include "libslic3r/AppConfig.hpp"
#include "libslic3r/Format/DRC.hpp"
#include "libslic3r/CAD/SketchEngine.hpp"
#include <wx/language.h>
#include "OG_CustomCtrl.hpp"
#include "wx/graphics.h"
@@ -1765,13 +1764,6 @@ void PreferencesDialog::create_items()
"parametrically. This feature is experimental and still under development."),
"enable_cad_feature", _L("(Requires restart)"));
g_sizer->Add(item_cad_feature);
auto item_auto_close_sketch_loops = create_item_checkbox(_L("Auto-close sketch loops"),
_L("Treat sketch endpoints within 0.001 mm as one joint and weld the loop shut. "
"Off: only exactly coincident endpoints join, so a loop with a tiny gap is "
"shown as open instead of being closed for you."),
"auto_close_sketch_loops");
g_sizer->Add(item_auto_close_sketch_loops);
#endif
#if 0
@@ -1861,11 +1853,18 @@ void PreferencesDialog::create_items()
"disc with a roll quadrant. A face's orientation is read without being learned. "
"Turn this off for the conventional CAD representation."), "design_connector_face_glyph");
g_sizer->Add(item_connector_face_glyph);
}
// Push the weld preference into the kernel now so toggling it takes effect without
// a restart (the sketch tool also re-pushes on activation, see DesignSketchTool::begin).
Slic3r::set_sketch_auto_close(wxGetApp().is_auto_close_sketch_loops());
// Saved WITH each design (it decides which loops are closed, i.e. what solid a project
// rebuilds into), so it applies to designs started from now on; an open design keeps
// the rule it was made with.
auto item_auto_close_sketch_loops = create_item_checkbox(_L("Auto-close sketch loops"),
_L("Treat sketch endpoints within 0.001 mm as one joint and weld the loop shut. "
"Off: only exactly coincident endpoints join, so a loop with a tiny gap is "
"shown as open instead of being closed for you. Saved with each design; "
"applies to designs started after the change."),
"auto_close_sketch_loops");
g_sizer->Add(item_auto_close_sketch_loops);
}
#endif
std::vector<wxString> ButtonDragActions = {_L("None"), _L("Pan"), _L("Rotate")};
+28 -1
View File
@@ -8657,12 +8657,13 @@ TEST_CASE("Hole standards: inch sizes by either name, with their 82° countersin
CHECK(doc.features[m].hole_cbore_diameter == 11.0);
}
TEST_CASE("Body colours and names survive a save and a load", "[CadDocument][recipe]")
TEST_CASE("Body colours, the modeling origin and the weld rule survive a save and a load", "[CadDocument][recipe]")
{
CadDocument doc;
const int sk = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 10, 10, 0, "S");
doc.add_extrude(sk, 5, false, BooleanMode::New, "E");
doc.modeling_origin = Vec3d(110, 120, 0);
doc.auto_close_loops = false; // the design's weld rule travels with it
REQUIRE(doc.recompute());
doc.bodies[0].has_color = true;
doc.bodies[0].color = ColorRGBA(0.1f, 0.2f, 0.3f, 1.0f);
@@ -8675,6 +8676,9 @@ TEST_CASE("Body colours and names survive a save and a load", "[CadDocument][rec
CHECK(std::abs(back.bodies[0].color.g() - 0.2f) < 1e-6);
CHECK((back.modeling_origin - Vec3d(110, 120, 0)).norm() < 1e-9); // the project's own
CHECK(back.origin_from_recipe);
CHECK_FALSE(back.auto_close_loops);
back.auto_close_loops = true; // leave the kernel's global as the other tests expect it
back.recompute();
}
TEST_CASE("A new thread reads its radius as the nominal (major) radius", "[CadDocument][thread]")
@@ -8692,3 +8696,26 @@ TEST_CASE("A new thread reads its radius as the nominal (major) radius", "[CadDo
CHECK_FALSE(doc.recompute());
CHECK(doc.error.find("thread") != std::string::npos);
}
TEST_CASE("CadDocument: a profile on a body face touches it, one in free space does not", "[CadDocument]")
{
// The Extrude default reads this: a profile drawn on a body joins it (charter L6).
CadDocument doc;
const int base = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 20, 20, 10, "Base");
doc.add_extrude(base, 5.0, false, BooleanMode::New, "Block");
REQUIRE(doc.recompute());
REQUIRE(doc.bodies.size() == 1);
SketchPlane top = SketchPlane::XY();
top.origin = Vec3d(0, 0, 5); // the block's top face
const int on_face = doc.add_sketch(SketchShape::Rectangle, top, 6, 6, 3, "Boss");
SketchPlane above = SketchPlane::XY();
above.origin = Vec3d(0, 0, 30); // well clear of the block
const int in_air = doc.add_sketch(SketchShape::Rectangle, above, 6, 6, 3, "Floating");
REQUIRE(doc.recompute());
CHECK(doc.body_touching_sketch(on_face) == 0);
CHECK(doc.body_touching_sketch(in_air) == -1);
CHECK(doc.body_touching_sketch(-1) == -1);
CHECK(doc.body_touching_sketch(base + 1) == -1); // the extrude: not a sketch
}