diff --git a/docs/CAD/design_tab.md b/docs/CAD/design_tab.md index 170c5ed0ce..80a94373f6 100644 --- a/docs/CAD/design_tab.md +++ b/docs/CAD/design_tab.md @@ -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 diff --git a/docs/CAD/design_tab_pr_description.md b/docs/CAD/design_tab_pr_description.md index a6d67f0e7c..450afeec70 100644 --- a/docs/CAD/design_tab_pr_description.md +++ b/docs/CAD/design_tab_pr_description.md @@ -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 diff --git a/docs/CAD/design_tab_upstream_portability.md b/docs/CAD/design_tab_upstream_portability.md index a7e9cd84ed..ab5ddde957 100644 --- a/docs/CAD/design_tab_upstream_portability.md +++ b/docs/CAD/design_tab_upstream_portability.md @@ -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. diff --git a/docs/CAD/ux/interaction-model.md b/docs/CAD/ux/interaction-model.md index 2041978ea0..c6407161b2 100644 --- a/docs/CAD/ux/interaction-model.md +++ b/docs/CAD/ux/interaction-model.md @@ -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 diff --git a/docs/CAD/ux/mockups/gen_offer_table.py b/docs/CAD/ux/mockups/gen_offer_table.py index 9744f9288b..a1810b15d6 100644 --- a/docs/CAD/ux/mockups/gen_offer_table.py +++ b/docs/CAD/ux/mockups/gen_offer_table.py @@ -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 ", "", + "#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'])};", diff --git a/docs/CAD/ux/tool_atlas.json b/docs/CAD/ux/tool_atlas.json index 94709f2057..0754534dae 100644 --- a/docs/CAD/ux/tool_atlas.json +++ b/docs/CAD/ux/tool_atlas.json @@ -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." ] }, { diff --git a/localization/i18n/list.txt b/localization/i18n/list.txt index 296829814a..e95ec4e937 100644 --- a/localization/i18n/list.txt +++ b/localization/i18n/list.txt @@ -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 diff --git a/scripts/CAD/check-gui-context-menu.py b/scripts/CAD/check-gui-context-menu.py index 28e404ecf0..9e76fa9fe1 100644 --- a/scripts/CAD/check-gui-context-menu.py +++ b/scripts/CAD/check-gui-context-menu.py @@ -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]), diff --git a/src/libslic3r/CAD/CadDocument.cpp b/src/libslic3r/CAD/CadDocument.cpp index 22b84e0099..f315efba63 100644 --- a/src/libslic3r/CAD/CadDocument.cpp +++ b/src/libslic3r/CAD/CadDocument.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -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& 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 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(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 } diff --git a/src/libslic3r/CAD/CadDocument.hpp b/src/libslic3r/CAD/CadDocument.hpp index 6969473b69..b95c493496 100644 --- a/src/libslic3r/CAD/CadDocument.hpp +++ b/src/libslic3r/CAD/CadDocument.hpp @@ -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 diff --git a/src/libslic3r/CAD/SketchEngine.cpp b/src/libslic3r/CAD/SketchEngine.cpp index 8aa440d707..8133de1174 100644 --- a/src/libslic3r/CAD/SketchEngine.cpp +++ b/src/libslic3r/CAD/SketchEngine.cpp @@ -47,18 +47,21 @@ #include #include #include +#include #include #include 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 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 ---- diff --git a/src/libslic3r/CAD/SketchEngine.hpp b/src/libslic3r/CAD/SketchEngine.hpp index 0dd35ac123..f433b90bce 100644 --- a/src/libslic3r/CAD/SketchEngine.hpp +++ b/src/libslic3r/CAD/SketchEngine.hpp @@ -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); diff --git a/src/libslic3r/Format/bbs_3mf.cpp b/src/libslic3r/Format/bbs_3mf.cpp index 160c169d69..7ae79c6191 100644 --- a/src/libslic3r/Format/bbs_3mf.cpp +++ b/src/libslic3r/Format/bbs_3mf.cpp @@ -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); diff --git a/src/slic3r/GUI/CAD/DesignOffer.hpp b/src/slic3r/GUI/CAD/DesignOffer.hpp index ad1998d5c5..5ccb929be2 100644 --- a/src/slic3r/GUI/CAD/DesignOffer.hpp +++ b/src/slic3r/GUI/CAD/DesignOffer.hpp @@ -10,6 +10,10 @@ #include +#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; diff --git a/src/slic3r/GUI/CAD/DesignPanel.cpp b/src/slic3r/GUI/CAD/DesignPanel.cpp index 097d79a343..aef3d3e0a8 100644 --- a/src/slic3r/GUI/CAD/DesignPanel.cpp +++ b/src/slic3r/GUI/CAD/DesignPanel.cpp @@ -388,7 +388,7 @@ DesignPanel::DesignPanel(wxWindow* parent) sk_key('R', DesignSketchTool::Mode::CornerRect, _L("Rectangle — click two opposite corners")); sk_key('C', DesignSketchTool::Mode::CenterCircle, _L("Circle — click center, then radius")); sk_key('A', DesignSketchTool::Mode::ThreePointArc,_L("Arc — click start, end, then a point")); - sk_key('S', DesignSketchTool::Mode::Slot, _L("Slot — two centerline ends, then end radius")); + sk_key('S', DesignSketchTool::Mode::Slot, _L("Slot — two centerline ends, then the width")); sk_key('E', DesignSketchTool::Mode::Ellipse, _L("Ellipse — center, major end, minor point")); sk_key('B', DesignSketchTool::Mode::BSpline, _L("Spline — click control points")); sk_key('P', DesignSketchTool::Mode::Point, _L("Point — click to place")); @@ -480,7 +480,7 @@ DesignPanel::DesignPanel(wxWindow* parent) m_keys_feature[WXK_HOME] = [this] { if (!m_viewport) return; m_viewport->set_view("iso"); - set_status(_L("Isometric view, fitted")); + set_status(_L("Axonometric view, fitted")); }; // Commit to Plate and the bed toggle were mouse-only: a toolbar button and a checkbox with @@ -852,7 +852,7 @@ DesignPanel::DesignPanel(wxWindow* parent) reset_axis_refs(); open_tool(Tool::Axis); }, SHIFT('A')}, - {"design_point", _L("Coord Sys"), _L("Datum coordinate system (world point, or face + direction edge)"), + {"design_point", _L("Coordinate system"), _L("Datum coordinate system (world point, or face + direction edge)"), [this] { reset_coordsys_refs(); open_tool(Tool::CoordSys); @@ -938,7 +938,7 @@ DesignPanel::DesignPanel(wxWindow* parent) populate_plane_choices(m_mirror_plane); open_tool(Tool::Mirror); }, SHIFT('Z')}, - {"design_c_coincident", _L("Mate"), _L("Assembly: align two CoordSys features (fastened, planar, revolute, slider, cylindrical)"), + {"design_c_coincident", _L("Mate"), _L("Assembly: align two coordinate systems (fastened, planar, revolute, slider, cylindrical)"), [this] { open_tool(Tool::Mate); }, 0}, @@ -970,7 +970,7 @@ DesignPanel::DesignPanel(wxWindow* parent) _L("Cut — needs a solid body to slice")}); // Color — override the selected body's display colour (per-body, survives recompute). - auto* b_color = icon_btn("color_palette", _L("Color — set the selected body's display colour")); + auto* b_color = icon_btn("color_palette", _L("Color — set the selected body's display color")); b_color->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_set_body_color(); }); fadd("color", b_color); m_verb_actions["btn:colour"] = [this] { on_set_body_color(); }; @@ -1014,6 +1014,7 @@ DesignPanel::DesignPanel(wxWindow* parent) m_verb_actions["btn:delete_body"] = [this] { on_delete_body(); }; m_verb_actions["btn:edit"] = [this] { on_edit_feature(); }; m_verb_actions["btn:mass"] = [this] { on_mass_properties(); }; + m_verb_actions["btn:interference"] = [this] { on_check_interference(); }; // Reachable from the offer menu on a SELECTED SKETCH, not only from the toolbar icon. // A user evaluating against Onshape reported that "adding constraints seems to be // missing" — with nineteen constraint types and a solver shipped. The only paths in @@ -1058,7 +1059,7 @@ DesignPanel::DesignPanel(wxWindow* parent) // Hole / Thread — drilling into a solid (both face-aware) feat_dropdown("hole", "design_hole", _L("Hole / thread"), { - {"design_hole", _L("Hole"), _L("Drill a hole, centred on a picked face or placed on a plane"), + {"design_hole", _L("Hole"), _L("Drill a hole, centerd on a picked face or placed on a plane"), [this] { // #2: drill on the picked solid face, centred on it (origin = face centroid, // normal = inward). Otherwise fall back to the plane dropdown. m_hole_x/y then @@ -1281,7 +1282,7 @@ DesignPanel::DesignPanel(wxWindow* parent) {"design_tangentarc", DesignSketchTool::Mode::TangentArc, _L("Tangent arc"), _L("Click start (on the last entity) then end")}, {"design_arc_center", DesignSketchTool::Mode::CenterArc, _L("Center-point arc"), _L("Click center, then start, then a point for the end angle")} }); dropdown("design_slot", _L("Slot"), { - {"design_slot", DesignSketchTool::Mode::Slot, _L("Slot"), _L("Click two centerline ends, then a point for the end radius")}, + {"design_slot", DesignSketchTool::Mode::Slot, _L("Slot"), _L("Click two centerline ends, then a point for the width")}, {"design_slot_arc", DesignSketchTool::Mode::ArcSlot, _L("Arc slot"), _L("Click center, start, end, then a point for the width")} }); dropdown("design_ellipse", _L("Ellipse"), { {"design_ellipse", DesignSketchTool::Mode::Ellipse, _L("Ellipse"), _L("Click center, a major-axis end, then a point for the minor axis")}, @@ -1722,7 +1723,7 @@ DesignPanel::DesignPanel(wxWindow* parent) mform->Add(m_move_axis, 0, wxEXPAND); m_move_angle = make_spin(m_cards, 0.0, -360.0, 360.0); - mform->Add(new wxStaticText(m_cards, wxID_ANY, _L("Angle °")), 0, wxALIGN_CENTER_VERTICAL); + mform->Add(new wxStaticText(m_cards, wxID_ANY, _L("Angle (°)")), 0, wxALIGN_CENTER_VERTICAL); mform->Add(spin_frame(m_move_angle), 0, wxEXPAND); for (wxSpinCtrlDouble* sp : { m_move_dx, m_move_dy, m_move_dz, m_move_angle }) @@ -1778,7 +1779,7 @@ DesignPanel::DesignPanel(wxWindow* parent) eform->Add(spin_frame(m_distance2), 0, wxEXPAND); m_taper = make_spin(m_cards, 0.0, -89.0, 89.0); // draft angle (deg) - eform->Add(new wxStaticText(m_cards, wxID_ANY, _L("Taper °")), 0, wxALIGN_CENTER_VERTICAL); + eform->Add(new wxStaticText(m_cards, wxID_ANY, _L("Taper (°)")), 0, wxALIGN_CENTER_VERTICAL); eform->Add(spin_frame(m_taper), 0, wxEXPAND); m_mode = make_combo(m_cards); @@ -1971,7 +1972,7 @@ DesignPanel::DesignPanel(wxWindow* parent) auto* rform = two_col_form(); m_revolve_angle = make_spin(m_cards, 360.0, 1.0, 360.0); - rform->Add(new wxStaticText(m_cards, wxID_ANY, _L("Angle °")), 0, wxALIGN_CENTER_VERTICAL); + rform->Add(new wxStaticText(m_cards, wxID_ANY, _L("Angle (°)")), 0, wxALIGN_CENTER_VERTICAL); rform->Add(spin_frame(m_revolve_angle), 0, wxEXPAND); m_revolve_axis = make_combo(m_cards); @@ -1982,12 +1983,12 @@ DesignPanel::DesignPanel(wxWindow* parent) rform->Add(m_revolve_axis, 0, wxEXPAND); m_revolve_mode = make_combo(m_cards); - m_revolve_mode->Append(_L("New")); - m_revolve_mode->Append(_L("Add")); + m_revolve_mode->Append(_L("New body")); // same four words as Extrude + m_revolve_mode->Append(_L("Join")); m_revolve_mode->Append(_L("Cut")); m_revolve_mode->Append(_L("Intersect")); m_revolve_mode->SetSelection(0); - rform->Add(new wxStaticText(m_cards, wxID_ANY, _L("Mode")), 0, wxALIGN_CENTER_VERTICAL); + rform->Add(new wxStaticText(m_cards, wxID_ANY, _L("Result")), 0, wxALIGN_CENTER_VERTICAL); rform->Add(m_revolve_mode, 0, wxEXPAND); m_revolve_flip = new CheckBox(m_cards); @@ -2012,12 +2013,12 @@ DesignPanel::DesignPanel(wxWindow* parent) sform->Add(m_sweep_path, 0, wxEXPAND); m_sweep_mode = make_combo(m_cards); - m_sweep_mode->Append(_L("New")); - m_sweep_mode->Append(_L("Add")); + m_sweep_mode->Append(_L("New body")); // same four words as Extrude + m_sweep_mode->Append(_L("Join")); m_sweep_mode->Append(_L("Cut")); m_sweep_mode->Append(_L("Intersect")); m_sweep_mode->SetSelection(0); - sform->Add(new wxStaticText(m_cards, wxID_ANY, _L("Mode")), 0, wxALIGN_CENTER_VERTICAL); + sform->Add(new wxStaticText(m_cards, wxID_ANY, _L("Result")), 0, wxALIGN_CENTER_VERTICAL); sform->Add(m_sweep_mode, 0, wxEXPAND); m_box_sweep->Add(sform, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, 12); @@ -2054,7 +2055,7 @@ DesignPanel::DesignPanel(wxWindow* parent) pform->Add(m_pattern_dir, 0, wxEXPAND); m_pattern_angle = make_spin(m_cards, 360.0, 1.0, 360.0); - pform->Add(new wxStaticText(m_cards, wxID_ANY, _L("Total angle°")), 0, wxALIGN_CENTER_VERTICAL); + pform->Add(new wxStaticText(m_cards, wxID_ANY, _L("Total angle (°)")), 0, wxALIGN_CENTER_VERTICAL); pform->Add(spin_frame(m_pattern_angle), 0, wxEXPAND); m_box_pattern->Add(pform, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, 12); @@ -2069,8 +2070,8 @@ DesignPanel::DesignPanel(wxWindow* parent) auto* bform = two_col_form(); m_bool_op = make_combo(m_cards); - m_bool_op->Append(_L("Union (join)")); - m_bool_op->Append(_L("Subtract (cut)")); + m_bool_op->Append(_L("Join")); // the Extrude result word; the offer row says the same + m_bool_op->Append(_L("Subtract")); m_bool_op->Append(_L("Intersect")); m_bool_op->SetSelection(0); m_bool_op->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent&) { refresh_preview(); }); @@ -2140,7 +2141,7 @@ DesignPanel::DesignPanel(wxWindow* parent) m_box_insert->Add(card_header(m_cards, "design_text", _L("Insert"), m_hdr_insert), 0, wxLEFT | wxRIGHT | wxTOP, 12); m_box_insert->Add(new wxStaticLine(m_cards), 0, wxEXPAND | wxALL, 8); m_box_insert->Add(new wxStaticText(m_cards, wxID_ANY, - _L("Drag a corner to size, the centre to move.\nConfirm or Cancel in the toolbar above.")), + _L("Drag a corner to size, the center to move.\nConfirm or Cancel in the toolbar above.")), 0, wxLEFT | wxRIGHT | wxBOTTOM, 12); cards->Add(m_box_insert, 0, wxEXPAND); @@ -2176,7 +2177,7 @@ DesignPanel::DesignPanel(wxWindow* parent) plform->Add(spin_frame(m_plane_offset), 0, wxEXPAND); m_plane_tilt = make_spin(m_cards, 0.0, -180.0, 180.0); - plform->Add(new wxStaticText(m_cards, wxID_ANY, _L("Angle°")), 0, wxALIGN_CENTER_VERTICAL); + plform->Add(new wxStaticText(m_cards, wxID_ANY, _L("Angle (°)")), 0, wxALIGN_CENTER_VERTICAL); plform->Add(spin_frame(m_plane_tilt), 0, wxEXPAND); m_plane_tilt_axis = make_combo(m_cards); @@ -2223,12 +2224,12 @@ DesignPanel::DesignPanel(wxWindow* parent) auto* lform = two_col_form(); m_loft_mode = make_combo(m_cards); - m_loft_mode->Append(_L("New")); - m_loft_mode->Append(_L("Add")); + m_loft_mode->Append(_L("New body")); // same four words as Extrude + m_loft_mode->Append(_L("Join")); m_loft_mode->Append(_L("Cut")); m_loft_mode->Append(_L("Intersect")); m_loft_mode->SetSelection(0); - lform->Add(new wxStaticText(m_cards, wxID_ANY, _L("Mode")), 0, wxALIGN_CENTER_VERTICAL); + lform->Add(new wxStaticText(m_cards, wxID_ANY, _L("Result")), 0, wxALIGN_CENTER_VERTICAL); lform->Add(m_loft_mode, 0, wxEXPAND); m_box_loft->Add(lform, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, 12); @@ -2267,7 +2268,7 @@ DesignPanel::DesignPanel(wxWindow* parent) { auto* rform = two_col_form(); m_surf_revolve_angle = make_spin(m_cards, 360.0, 1.0, 360.0); - rform->Add(new wxStaticText(m_cards, wxID_ANY, _L("Angle °")), 0, wxALIGN_CENTER_VERTICAL); + rform->Add(new wxStaticText(m_cards, wxID_ANY, _L("Angle (°)")), 0, wxALIGN_CENTER_VERTICAL); rform->Add(spin_frame(m_surf_revolve_angle), 0, wxEXPAND); m_surf_revolve_axis = make_combo(m_cards); m_surf_revolve_axis->Append(_L("Plane X")); @@ -2604,7 +2605,7 @@ DesignPanel::DesignPanel(wxWindow* parent) wxString s; for (size_t i = 0; i < m_del_faces.size(); ++i) { if (i > 0) s += ", "; - s += wxString::Format("Face %d", m_del_faces[i]); + s += wxString::Format(_L("Face %d"), m_del_faces[i]); } m_del_face_list->SetLabel(s.empty() ? _L("(none)") : s); m_del_face_list->GetParent()->Layout(); @@ -2727,7 +2728,7 @@ DesignPanel::DesignPanel(wxWindow* parent) // --- CoordSys (datum coordinate system: point + orthonormal frame) --- m_box_coordsys = new wxBoxSizer(wxVERTICAL); - m_box_coordsys->Add(card_header(m_cards, "design_point", _L("Coord Sys"), m_hdr_coordsys), 0, wxLEFT | wxRIGHT | wxTOP, 12); + m_box_coordsys->Add(card_header(m_cards, "design_point", _L("Coordinate system"), m_hdr_coordsys), 0, wxLEFT | wxRIGHT | wxTOP, 12); m_box_coordsys->Add(new wxStaticLine(m_cards), 0, wxEXPAND | wxALL, 8); { m_coordsys_type = make_combo(m_cards); @@ -2797,7 +2798,7 @@ DesignPanel::DesignPanel(wxWindow* parent) } cards->Add(m_box_coordsys, 0, wxEXPAND); - // --- Mate (assembly: align two CoordSys features) --- + // --- Mate (assembly: align two coordinate systems) --- m_box_mate = new wxBoxSizer(wxVERTICAL); m_box_mate->Add(card_header(m_cards, "design_c_coincident", _L("Mate"), m_hdr_mate), 0, wxLEFT | wxRIGHT | wxTOP, 12); m_box_mate->Add(new wxStaticLine(m_cards), 0, wxEXPAND | wxALL, 8); @@ -2816,11 +2817,11 @@ DesignPanel::DesignPanel(wxWindow* parent) // Populate the CoordSys pickers on open; show "A (fixed)" and "B (moves)" combos. m_mate_cs_a = make_combo(m_cards); - mform->Add(new wxStaticText(m_cards, wxID_ANY, _L("CS A (fixed)")), 0, wxALIGN_CENTER_VERTICAL); + mform->Add(new wxStaticText(m_cards, wxID_ANY, _L("A (fixed)")), 0, wxALIGN_CENTER_VERTICAL); mform->Add(m_mate_cs_a, 0, wxEXPAND); m_mate_cs_b = make_combo(m_cards); - mform->Add(new wxStaticText(m_cards, wxID_ANY, _L("CS B (moves)")), 0, wxALIGN_CENTER_VERTICAL); + mform->Add(new wxStaticText(m_cards, wxID_ANY, _L("B (moves)")), 0, wxALIGN_CENTER_VERTICAL); mform->Add(m_mate_cs_b, 0, wxEXPAND); m_offset_label = new wxStaticText(m_cards, wxID_ANY, _L("Offset")); @@ -3247,7 +3248,7 @@ DesignPanel::DesignPanel(wxWindow* parent) bvis->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_toggle_visibility(); }); auto* bdel = body_btn("design_delete", _L("Delete")); bdel->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_delete_body(); }); - auto* bcol = body_btn("color_palette", _L("Colour")); + auto* bcol = body_btn("color_palette", _L("Color")); bcol->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_set_body_color(); }); const int bgap = FromDIP(SidebarProps::ElementSpacing()); m_parts_hdr->AddStretchSpacer(1); @@ -3465,7 +3466,7 @@ DesignPanel::DesignPanel(wxWindow* parent) m_viewport->set_on_sketch_commit([this](const SketchProfile& prof, const SketchPlane& plane) { m_doc.checkpoint(); // undo boundary: committing a sketch m_feature_counter++; - m_doc.add_sketch_profile(prof, plane, "Sketch" + std::to_string(m_feature_counter)); + m_doc.add_sketch_profile(prof, plane, feature_name(_L("Sketch"))); m_doc.recompute(); set_status(StatusKind::Info, _L("Sketch created — select it, then right-click to Extrude")); refresh_tree(); @@ -3500,7 +3501,7 @@ DesignPanel::DesignPanel(wxWindow* parent) } m_feature_counter++; const int sk = m_doc.add_sketch_entities(ents, plane, - "Sketch" + std::to_string(m_feature_counter), cons); + feature_name(_L("Sketch")), cons); if (!cons.empty()) m_doc.solve_sketch_feature(sk); // enforce driving dimensions m_doc.recompute(); set_status(StatusKind::Info, cons.empty() @@ -3517,8 +3518,8 @@ DesignPanel::DesignPanel(wxWindow* parent) // APPENDED to the step guidance, never in place of it. This fires on every mouse move // while a segment is being dragged, so replacing the line wiped the instruction for the // step the user is in the middle of — one mouse move after the click that armed it. - const wxString metrics = wxString::Format(L"L %.2f mm %.1f°%s", - len, a, locked ? L" (locked)" : L""); + wxString metrics = wxString::Format(_L("Length %.2f mm, angle %.1f°"), len, a); + if (locked) metrics += " " + _L("(locked)"); set_status(m_sketch_step.IsEmpty() ? metrics : m_sketch_step + L" · " + metrics); m_status->Refresh(); @@ -4722,7 +4723,7 @@ void DesignPanel::on_import_step() m_feature_counter++; CadFeature f; f.type = CadFeatureType::Import; - f.name = std::string("STEP") + std::to_string(m_feature_counter); + f.name = feature_name("STEP"); f.imported_solid = s; f.mode = BooleanMode::New; // each solid is its own coexisting body m_doc.features.push_back(f); @@ -4820,7 +4821,7 @@ void DesignPanel::on_import_mesh() m_feature_counter++; CadFeature f; f.type = CadFeatureType::Import; - f.name = std::string("Mesh") + std::to_string(m_feature_counter); + f.name = feature_name(_L("Mesh")); f.imported_solid = shape; f.mode = BooleanMode::New; // its own coexisting body, like a STEP solid m_doc.features.push_back(f); @@ -4873,7 +4874,7 @@ void DesignPanel::add_imported_sketch( m_feature_counter++; CadFeature f; f.type = CadFeatureType::Sketch; - f.name = std::string(base_name.ToUTF8().data()) + std::to_string(m_feature_counter); + f.name = feature_name(base_name); f.imported_regions = regions; // #4: when a solid face is selected, drop the art ON that face, centred on it (ready to @@ -4969,7 +4970,7 @@ void DesignPanel::on_transform_imported(int feat_idx) // the centre to move. Values stream back via set_on_imported_transform. m_viewport->begin_imported_transform(feat_idx, f.imported_regions, f.plane, f.import_offset, f.import_scale_x, f.import_scale_y); - set_status(StatusKind::Info, _L("Drag a corner to scale, the centre to move — Enter or ✓ when done, Esc to discard")); + set_status(StatusKind::Info, _L("Drag a corner to scale, the center to move — Enter or ✓ when done, Esc to discard")); } void DesignPanel::on_add_sketch() @@ -4980,7 +4981,7 @@ void DesignPanel::on_add_sketch() SketchPlane plane = sketch_plane_from_selection(where); // picked face, else the 3D plane click m_feature_counter++; m_doc.add_sketch(shape, plane, m_width->GetValue(), m_height->GetValue(), - m_radius->GetValue(), "Sketch" + std::to_string(m_feature_counter)); + m_radius->GetValue(), feature_name(_L("Sketch"))); m_doc.recompute(); // a lone sketch yields an empty body; that is expected set_status(StatusKind::Info, wxString::Format(_L("Sketch added on %s — select it, then right-click to Extrude"), where)); refresh_tree(); @@ -4998,11 +4999,18 @@ bool DesignPanel::extrude_uses_loop() const && !m_viewport->selected_loop_entities().empty(); } +// Default name of the feature being added: the card's own header word and number ("Extrude 3"), +// so the tree row and the card title that preceded it read the same, in the user's language. +std::string DesignPanel::feature_name(const wxString& kind) const +{ + return std::string((kind + wxString::Format(" %d", m_feature_counter)).ToUTF8().data()); +} + void DesignPanel::on_add_extrude() { BooleanMode mode = static_cast(m_mode->GetSelection()); // New/Add/Cut/Intersect m_feature_counter++; - const std::string name = "Extrude" + std::to_string(m_feature_counter); + const std::string name = feature_name(_L("Extrude")); int idx = -1; if (m_extrude_face_src >= 0) { // Onshape face-extrude: the picked solid face is the profile (no sketch wire). @@ -5032,7 +5040,8 @@ void DesignPanel::on_add_extrude() f.taper_deg = m_taper->GetValue(); f.flip = m_flip->GetValue(); f.up_to_face = (f.extrude_end == ExtrudeEnd::UpToFace) ? m_sel_solid_face : -1; - f.target_body = m_sel_solid_body; // multi-body: act on the picked body (-1 = last) + // multi-body: act on the picked body, else the body the profile touches (-1 = last) + f.target_body = m_sel_solid_body >= 0 ? m_sel_solid_body : m_extrude_auto_body; // On-face Text/SVG remembers its host body even after the face pick was cleared by // the placement recompute, so the engraving Cut hits the right solid. if (m_extrude_sketch_ref >= 0 && m_extrude_sketch_ref < int(m_doc.features.size()) @@ -5061,13 +5070,13 @@ void DesignPanel::on_add_dressup() int didx = -1; if (m_sel_solid_edge >= 0) { if (fillet) - didx = m_doc.add_fillet(sz, m_sel_solid_edge, "Fillet" + std::to_string(m_feature_counter)); + didx = m_doc.add_fillet(sz, m_sel_solid_edge, feature_name(_L("Fillet"))); else - didx = m_doc.add_chamfer(sz, m_sel_solid_edge, "Chamfer" + std::to_string(m_feature_counter)); + didx = m_doc.add_chamfer(sz, m_sel_solid_edge, feature_name(_L("Chamfer"))); } else if (fillet) - didx = m_doc.add_fillet(sz, fg, "Fillet" + std::to_string(m_feature_counter)); + didx = m_doc.add_fillet(sz, fg, feature_name(_L("Fillet"))); else - didx = m_doc.add_chamfer(sz, fg, "Chamfer" + std::to_string(m_feature_counter)); + didx = m_doc.add_chamfer(sz, fg, feature_name(_L("Chamfer"))); // Dress the picked body (its face/edge ids are body-local). -1 = last body. if (didx >= 0 && didx < int(m_doc.features.size())) m_doc.features[didx].target_body = m_sel_solid_body; @@ -5129,7 +5138,7 @@ void DesignPanel::on_add_hole() m_feature_counter++; const int hidx = m_doc.add_hole(dia, depth, through, px, py, plane, - "Hole" + std::to_string(m_feature_counter)); + feature_name(_L("Hole"))); // On-face holes drill the body the face belongs to (even after the pick was cleared). if (m_hole_on_face && hidx >= 0 && hidx < int(m_doc.features.size())) m_doc.features[hidx].target_body = m_hole_face_body; @@ -5198,7 +5207,7 @@ void DesignPanel::on_add_thread() { bool internal = m_thread_internal->GetValue(); if (internal && m_doc.body.IsNull()) { - set_status(_L("Thread needs a solid body — add or import one first")); + set_status(StatusKind::Warning, _L("Thread needs a solid body — add or import one first")); return; } SketchPlane plane = thread_plane(); @@ -5207,7 +5216,7 @@ void DesignPanel::on_add_thread() const int tidx = m_doc.add_thread(m_thread_radius->GetValue() * 0.5, m_thread_pitch->GetValue(), m_thread_height->GetValue(), m_thread_depth->GetValue(), internal, m_thread_x->GetValue(), m_thread_y->GetValue(), - plane, "Thread" + std::to_string(m_feature_counter)); + plane, feature_name(_L("Thread"))); // On-surface internal thread taps the body the cylindrical face belongs to. if (m_thread_on_face && tidx >= 0 && tidx < int(m_doc.features.size())) m_doc.features[tidx].target_body = m_thread_face_body; @@ -5228,13 +5237,13 @@ void DesignPanel::on_add_revolve() } const BooleanMode mode = static_cast(m_revolve_mode->GetSelection()); if (mode != BooleanMode::New && m_doc.body.IsNull()) { - set_status(_L("Revolve needs a solid body — add or import one first")); + set_status(StatusKind::Warning, _L("Revolve needs a solid body — add or import one first")); return; } m_feature_counter++; m_doc.add_revolve(m_revolve_sketch_ref, m_revolve_angle->GetValue(), m_revolve_axis->GetSelection(), m_revolve_flip->GetValue(), - mode, "Revolve" + std::to_string(m_feature_counter)); + mode, feature_name(_L("Revolve"))); if (!recompute_guarded(_L("Rebuilding model…"))) set_status(StatusKind::Error, wxString::Format(_L("The model could not be rebuilt: %s"), kernel_error_text(m_doc.error))); @@ -5259,12 +5268,12 @@ void DesignPanel::on_add_sweep() } const BooleanMode mode = static_cast(m_sweep_mode->GetSelection()); if (mode != BooleanMode::New && m_doc.body.IsNull()) { - set_status(_L("Sweep needs a solid body — add or import one first")); + set_status(StatusKind::Warning, _L("Sweep needs a solid body — add or import one first")); return; } m_feature_counter++; m_doc.add_sweep(m_sweep_profile_ref, path_ref, mode, - "Sweep" + std::to_string(m_feature_counter)); + feature_name(_L("Sweep"))); if (!recompute_guarded(_L("Rebuilding model…"))) set_status(StatusKind::Error, wxString::Format(_L("The model could not be rebuilt: %s"), kernel_error_text(m_doc.error))); @@ -5287,12 +5296,12 @@ void DesignPanel::on_add_loft() } const BooleanMode mode = static_cast(m_loft_mode->GetSelection()); if (mode != BooleanMode::New && m_doc.body.IsNull()) { - set_status(_L("Loft needs a solid body — add or import one first")); + set_status(StatusKind::Warning, _L("Loft needs a solid body — add or import one first")); return; } m_feature_counter++; m_doc.add_loft(refs, m_loft_ruled->GetValue(), mode, - "Loft" + std::to_string(m_feature_counter)); + feature_name(_L("Loft"))); if (!recompute_guarded(_L("Rebuilding model…"))) set_status(StatusKind::Error, wxString::Format(_L("The model could not be rebuilt: %s"), kernel_error_text(m_doc.error))); @@ -5310,7 +5319,7 @@ void DesignPanel::on_add_surface_extrude() } m_feature_counter++; m_doc.add_surface_extrude(m_surf_extrude_sketch_ref, m_surf_extrude_distance->GetValue(), - "SurfaceExtrude" + std::to_string(m_feature_counter)); + feature_name(_L("Surface Extrude"))); if (!recompute_guarded(_L("Rebuilding model…"))) set_status(StatusKind::Error, wxString::Format(_L("The model could not be rebuilt: %s"), kernel_error_text(m_doc.error))); else @@ -5327,7 +5336,7 @@ void DesignPanel::on_add_surface_revolve() m_feature_counter++; m_doc.add_surface_revolve(m_surf_revolve_sketch_ref, m_surf_revolve_angle->GetValue(), m_surf_revolve_axis->GetSelection(), - "SurfaceRevolve" + std::to_string(m_feature_counter)); + feature_name(_L("Surface Revolve"))); if (!recompute_guarded(_L("Rebuilding model…"))) set_status(StatusKind::Error, wxString::Format(_L("The model could not be rebuilt: %s"), kernel_error_text(m_doc.error))); else @@ -5347,7 +5356,7 @@ void DesignPanel::on_add_surface_loft() } m_feature_counter++; m_doc.add_surface_loft(refs, m_surf_loft_ruled->GetValue(), - "SurfaceLoft" + std::to_string(m_feature_counter)); + feature_name(_L("Surface Loft"))); if (!recompute_guarded(_L("Rebuilding model…"))) set_status(StatusKind::Error, wxString::Format(_L("The model could not be rebuilt: %s"), kernel_error_text(m_doc.error))); else @@ -5363,7 +5372,7 @@ void DesignPanel::on_add_surface_fill() } m_feature_counter++; m_doc.add_surface_fill(m_surf_fill_sketch_ref, - "SurfaceFill" + std::to_string(m_feature_counter)); + feature_name(_L("Surface Fill"))); if (!recompute_guarded(_L("Rebuilding model…"))) set_status(StatusKind::Error, wxString::Format(_L("The model could not be rebuilt: %s"), kernel_error_text(m_doc.error))); else @@ -5380,7 +5389,7 @@ void DesignPanel::on_add_surface_offset() } m_feature_counter++; m_doc.add_surface_offset(sel, m_surf_offset_distance->GetValue(), - "SurfaceOffset" + std::to_string(m_feature_counter)); + feature_name(_L("Surface Offset"))); if (!recompute_guarded(_L("Rebuilding model…"))) set_status(StatusKind::Error, wxString::Format(_L("The model could not be rebuilt: %s"), kernel_error_text(m_doc.error))); else @@ -5398,7 +5407,7 @@ void DesignPanel::on_add_thicken_surface() m_feature_counter++; m_doc.add_thicken_surface(sel, m_surf_thicken_thickness->GetValue(), m_surf_thicken_flip->GetValue(), - "ThickenSurface" + std::to_string(m_feature_counter)); + feature_name(_L("Thicken Surface"))); if (!recompute_guarded(_L("Rebuilding model…"))) set_status(StatusKind::Error, wxString::Format(_L("The model could not be rebuilt: %s"), kernel_error_text(m_doc.error))); else @@ -5483,7 +5492,7 @@ void DesignPanel::on_add_transform() m_move_body = -1; } if (m_doc.bodies.empty()) { - set_status(_L("Transform needs a body — add or import one first")); + set_status(StatusKind::Warning, _L("Transform needs a body — add or import one first")); return; } const int sel = m_xf_body->GetSelection(); @@ -5494,7 +5503,7 @@ void DesignPanel::on_add_transform() const Vec3d pivot(m_xf_pivot_x->GetValue(), m_xf_pivot_y->GetValue(), m_xf_pivot_z->GetValue()); m_feature_counter++; m_doc.add_transform(target, trans, axis, pivot, m_xf_angle->GetValue(), m_xf_copy->GetValue(), - "Transform" + std::to_string(m_feature_counter)); + feature_name(_L("Transform"))); if (!recompute_guarded(_L("Rebuilding model…"))) set_status(StatusKind::Error, wxString::Format(_L("The model could not be rebuilt: %s"), kernel_error_text(m_doc.error))); else @@ -5505,7 +5514,7 @@ void DesignPanel::on_add_transform() void DesignPanel::on_add_mirror() { if (m_doc.bodies.empty()) { - set_status(_L("Mirror needs a body — add or import one first")); + set_status(StatusKind::Warning, _L("Mirror needs a body — add or import one first")); return; } const int sel = m_mirror_body->GetSelection(); @@ -5513,7 +5522,7 @@ void DesignPanel::on_add_mirror() const BooleanMode mode = m_mirror_keep->GetValue() ? BooleanMode::New : BooleanMode::Add; m_feature_counter++; m_doc.add_mirror(plane_from_choice(m_mirror_plane->GetSelection()), target, mode, - "Mirror" + std::to_string(m_feature_counter)); + feature_name(_L("Mirror"))); if (!recompute_guarded(_L("Rebuilding model…"))) set_status(StatusKind::Error, wxString::Format(_L("The model could not be rebuilt: %s"), kernel_error_text(m_doc.error))); else @@ -5524,7 +5533,7 @@ void DesignPanel::on_add_mirror() void DesignPanel::on_add_thicken() { if (m_doc.bodies.empty()) { - set_status(_L("Thicken needs a solid body — add or import one first")); + set_status(StatusKind::Warning, _L("Thicken needs a solid body — add or import one first")); return; } if (m_sel_solid_face < 0) { @@ -5535,7 +5544,7 @@ void DesignPanel::on_add_thicken() const int target = (sel != wxNOT_FOUND) ? sel : -1; m_feature_counter++; m_doc.add_thicken(target, m_sel_solid_face, m_thicken_thickness->GetValue(), - m_thicken_flip->GetValue(), "Thicken" + std::to_string(m_feature_counter)); + m_thicken_flip->GetValue(), feature_name(_L("Thicken"))); if (!recompute_guarded(_L("Rebuilding model…"))) set_status(StatusKind::Error, wxString::Format(_L("The model could not be rebuilt: %s"), kernel_error_text(m_doc.error))); else @@ -5546,7 +5555,7 @@ void DesignPanel::on_add_thicken() void DesignPanel::on_add_rib() { if (m_doc.bodies.empty()) { - set_status(_L("Rib needs a solid body — add or import one first")); + set_status(StatusKind::Warning, _L("Rib needs a solid body — add or import one first")); return; } const int bsel = m_rib_body->GetSelection(); @@ -5560,7 +5569,7 @@ void DesignPanel::on_add_rib() } m_feature_counter++; m_doc.add_rib(sketch_ref, m_rib_entity->GetValue(), m_rib_thickness->GetValue(), - m_rib_depth->GetValue(), target, "Rib" + std::to_string(m_feature_counter)); + m_rib_depth->GetValue(), target, feature_name(_L("Rib"))); if (!recompute_guarded(_L("Rebuilding model…"))) set_status(StatusKind::Error, wxString::Format(_L("The model could not be rebuilt: %s"), kernel_error_text(m_doc.error))); else @@ -5571,7 +5580,7 @@ void DesignPanel::on_add_rib() void DesignPanel::on_add_project() { if (m_doc.bodies.empty()) { - set_status(_L("Project needs a body — add or import one first")); + set_status(StatusKind::Warning, _L("Project needs a body — add or import one first")); return; } const int sel = m_proj_source_body->GetSelection(); @@ -5580,7 +5589,7 @@ void DesignPanel::on_add_project() m_feature_counter++; m_doc.add_project_edges(src_body, {}, face, plane_from_choice(m_proj_plane->GetSelection()), - "Project" + std::to_string(m_feature_counter)); + feature_name(_L("Project"))); if (!recompute_guarded(_L("Rebuilding model…"))) set_status(StatusKind::Error, wxString::Format(_L("The model could not be rebuilt: %s"), kernel_error_text(m_doc.error))); else @@ -5591,7 +5600,7 @@ void DesignPanel::on_add_project() void DesignPanel::on_add_delete_face() { if (m_doc.bodies.empty()) { - set_status(_L("Delete Face needs a body — add or import one first")); + set_status(StatusKind::Warning, _L("Delete Face needs a body — add or import one first")); return; } if (m_del_faces.empty()) { @@ -5601,7 +5610,7 @@ void DesignPanel::on_add_delete_face() const int sel = m_del_face_body->GetSelection(); const int target = (sel != wxNOT_FOUND) ? sel : -1; m_feature_counter++; - m_doc.add_delete_face(target, m_del_faces, "DeleteFace" + std::to_string(m_feature_counter)); + m_doc.add_delete_face(target, m_del_faces, feature_name(_L("Delete Face"))); m_del_faces.clear(); // consumed; fresh state for the next use if (!recompute_guarded(_L("Rebuilding model…"))) set_status(StatusKind::Error, wxString::Format(_L("The model could not be rebuilt: %s"), kernel_error_text(m_doc.error))); @@ -5616,7 +5625,7 @@ void DesignPanel::on_add_helix() m_doc.add_helix(plane_from_choice(m_helix_plane->GetSelection()), m_helix_radius->GetValue(), m_helix_pitch->GetValue(), m_helix_height->GetValue(), m_helix_left_handed->GetValue(), - m_helix_taper->GetValue(), "Helix" + std::to_string(m_feature_counter)); + m_helix_taper->GetValue(), feature_name(_L("Helix"))); if (!recompute_guarded(_L("Rebuilding model…"))) set_status(StatusKind::Error, wxString::Format(_L("The model could not be rebuilt: %s"), kernel_error_text(m_doc.error))); else @@ -5629,20 +5638,20 @@ void DesignPanel::on_add_mate() const int sel_a = m_mate_cs_a->GetSelection(); const int sel_b = m_mate_cs_b->GetSelection(); if (sel_a == wxNOT_FOUND || sel_b == wxNOT_FOUND) { - set_status(StatusKind::Error, _L("Mate needs two CoordSys features — create them first")); + set_status(StatusKind::Warning, _L("Mate needs two coordinate systems — create them first")); return; } const int cs_a = int(reinterpret_cast(m_mate_cs_a->GetClientData(sel_a))); const int cs_b = int(reinterpret_cast(m_mate_cs_b->GetClientData(sel_b))); if (cs_a == cs_b) { - set_status(StatusKind::Error, _L("Mate: CS A and CS B must be different CoordSys features")); + set_status(StatusKind::Error, _L("Mate: A and B must be different coordinate systems")); return; } m_feature_counter++; int idx = m_doc.add_mate(m_mate_kind->GetSelection(), cs_a, cs_b, m_mate_offset->GetValue(), m_mate_angle->GetValue(), m_mate_flip->GetValue(), - "Mate" + std::to_string(m_feature_counter)); + feature_name(_L("Mate"))); if (idx < 0) { set_status(StatusKind::Error, _L("Mate rejected")); return; @@ -5657,72 +5666,60 @@ void DesignPanel::on_add_mate() void DesignPanel::on_check_interference() { if (m_doc.bodies.size() < 2) { - set_status(StatusKind::Info, _L("No interference — need at least two solid bodies to check")); + set_status(StatusKind::Info, _L("Interference needs at least two bodies")); return; } const auto pairs = m_doc.check_interference(); if (pairs.empty()) { - set_status(StatusKind::Info, _L("No interference found")); + set_status(StatusKind::Ok, _L("No interference found")); return; } - double worst = 0; - for (const auto& p : pairs) - if (p.volume > worst) worst = p.volume; - set_status(StatusKind::Info, wxString::Format(_L("%zu interference pairs, worst %.2f mm³"), - pairs.size(), worst)); - wxString msg = _L("Interference pairs:\n\n"); - for (const auto& p : pairs) { - // 1-based, like every other body label in this panel and in the parts tree: - // reporting "Body 1" for what the tree calls "Body 2" is worse than no name. - wxString na = wxString::Format(_L("Body %d"), p.body_a + 1); - wxString nb = wxString::Format(_L("Body %d"), p.body_b + 1); - if (p.body_a >= 0 && p.body_a < int(m_doc.bodies.size()) && !m_doc.bodies[p.body_a].name.empty()) - na = wxString::FromUTF8(m_doc.bodies[p.body_a].name); - if (p.body_b >= 0 && p.body_b < int(m_doc.bodies.size()) && !m_doc.bodies[p.body_b].name.empty()) - nb = wxString::FromUTF8(m_doc.bodies[p.body_b].name); - msg += wxString::Format("%s <-> %s: %.4f mm³\n", na, nb, p.volume); - } - wxMessageBox(msg, _L("Interference"), wxOK, this); + // A report, so it goes to the status line like every other report — no modal to dismiss. + // Worst overlap first; 1-based names, the wording the parts tree uses. + auto body_name = [this](int b) { + if (b >= 0 && b < int(m_doc.bodies.size()) && !m_doc.bodies[b].name.empty()) + return wxString::FromUTF8(m_doc.bodies[b].name); + return wxString::Format(_L("Body %d"), b + 1); + }; + auto sorted = pairs; + std::sort(sorted.begin(), sorted.end(), [](const auto& a, const auto& b) { return a.volume > b.volume; }); + wxString list; + const size_t shown = std::min(sorted.size(), 3); + for (size_t k = 0; k < shown; ++k) + list += (k ? ", " : "") + wxString::Format(_L("%s ↔ %s %.2f mm³"), body_name(sorted[k].body_a), + body_name(sorted[k].body_b), sorted[k].volume); + if (sorted.size() > shown) + list += ", …"; + set_status(StatusKind::Warning, wxString::Format(_L("%zu overlapping pairs: %s"), sorted.size(), list)); } -// Mass properties of the selected solid. A report, not a feature: it never checkpoints, never -// recomputes and never opens a card, which is why it sits beside the interference check rather -// than in the on_add_* family. The caller only reaches us with m_sel_solid_body in range. +// Volume and surface area of the selected body. A report, not a feature: it never checkpoints, +// never recomputes and never opens a card, which is why it sits beside the interference check +// rather than in the on_add_* family. It reports geometry only — there is no density, so no mass. void DesignPanel::on_mass_properties() { // This bounds check is not defensive padding — it is what makes the verb safe to fire from - // the socket, which has no offer menu to grey the row out. The menu-only route never reached - // here with nothing selected; run_verb does. Nothing selected is not an error, hence the - // neutral colour, not the error red. + // the socket, which has no offer menu to grey the row out. if (m_sel_solid_body < 0 || m_sel_solid_body >= int(m_doc.bodies.size())) { - set_status(StatusKind::Info, _L("Select a solid body first — its mass properties are what is reported")); + set_status(StatusKind::Info, _L("Select a body to measure it")); return; } const auto mp = GeometryEngine::mass_properties(m_doc.bodies[m_sel_solid_body].shape); if (!mp.valid) { - set_status(StatusKind::Error, _L("Mass properties could not be computed for this body")); + set_status(StatusKind::Error, _L("This body could not be measured")); return; } - // 1-based, and the body's own name when it has one — the same wording the parts list uses. wxString name = wxString::Format(_L("Body %d"), m_sel_solid_body + 1); if (!m_doc.bodies[m_sel_solid_body].name.empty()) name = wxString::FromUTF8(m_doc.bodies[m_sel_solid_body].name); - if (!mp.is_solid) { + // Same units and precision as the interference report: mm³ and mm², two decimals. + if (!mp.is_solid) // Sheet body: quoting a volume here would be inventing material that is not there. - set_status(StatusKind::Info, wxString::Format(_L("%s: sheet body — %.2f cm² of surface, no volume"), - name, mp.surface_area / 100.0)); - wxMessageBox(wxString::Format(_L("%s\n\nSheet body (open shell)\nSurface area: %.2f cm²\n\n" - "A sheet encloses no material, so it has no volume. " - "Thicken it into a solid to get one."), - name, mp.surface_area / 100.0), - _L("Mass properties"), wxOK, this); - return; - } - set_status(StatusKind::Info, wxString::Format(_L("%s: %.3f cm³, %.2f cm²"), - name, mp.volume / 1000.0, mp.surface_area / 100.0)); - wxMessageBox(wxString::Format(_L("%s\n\nVolume: %.3f cm³\nSurface area: %.2f cm²"), - name, mp.volume / 1000.0, mp.surface_area / 100.0), - _L("Mass properties"), wxOK, this); + set_status(StatusKind::Info, wxString::Format(_L("%s: sheet body, area %.2f mm², no volume — thicken it to get one"), + name, mp.surface_area)); + else + set_status(StatusKind::Info, wxString::Format(_L("%s: volume %.2f mm³, area %.2f mm²"), + name, mp.volume, mp.surface_area)); } // The rows are only the SHEET bodies, so a row index is NOT a body index — with a solid at 0 @@ -5768,7 +5765,7 @@ void DesignPanel::select_sheet_choice(ComboBox* c, int body) void DesignPanel::on_add_pattern() { if (m_doc.bodies.empty()) { - set_status(_L("Pattern needs a solid body — add or import one first")); + set_status(StatusKind::Warning, _L("Pattern needs a solid body — add or import one first")); return; } const bool circular = (m_pattern_type->GetSelection() == 1); @@ -5778,7 +5775,7 @@ void DesignPanel::on_add_pattern() m_doc.add_pattern(circular, int(m_pattern_count->GetValue()), m_pattern_spacing->GetValue(), m_pattern_dir->GetSelection(), m_pattern_angle->GetValue(), target, - "Pattern" + std::to_string(m_feature_counter)); + feature_name(_L("Pattern"))); if (!recompute_guarded(_L("Rebuilding model…"))) set_status(StatusKind::Error, wxString::Format(_L("The model could not be rebuilt: %s"), kernel_error_text(m_doc.error))); @@ -5853,7 +5850,7 @@ void DesignPanel::fill_body_choice(ComboBox* c, int as_of_feature, int want) void DesignPanel::on_add_boolean() { if (m_doc.bodies.size() < 2) { - set_status(_L("Boolean needs two solid bodies — add or import a second one")); + set_status(StatusKind::Warning, _L("Boolean needs two bodies — add or import a second one")); return; } const int sel = m_bool_op->GetSelection(); @@ -5863,7 +5860,7 @@ void DesignPanel::on_add_boolean() m_feature_counter++; m_doc.add_boolean(op, m_bool_target->GetSelection(), m_bool_tool->GetSelection(), m_bool_keep->GetValue(), m_bool_tol->GetValue(), -1, -1, - "Boolean" + std::to_string(m_feature_counter)); + feature_name(_L("Boolean"))); if (!recompute_guarded(_L("Rebuilding model…"))) set_status(StatusKind::Error, wxString::Format(_L("The model could not be rebuilt: %s"), kernel_error_text(m_doc.error))); else @@ -5874,13 +5871,13 @@ void DesignPanel::on_add_boolean() void DesignPanel::on_add_cut() { if (m_doc.bodies.empty()) { - set_status(_L("Cut needs a solid body — add or import one first")); + set_status(StatusKind::Warning, _L("Cut needs a solid body — add or import one first")); return; } m_feature_counter++; m_doc.add_cut(plane_from_choice(m_cut_plane->GetSelection()), m_cut_offset->GetValue(), /*flip*/ false, /*keep_upper*/ true, /*keep_lower*/ true, - m_cut_target->GetSelection(), "Cut" + std::to_string(m_feature_counter)); + m_cut_target->GetSelection(), feature_name(_L("Cut"))); if (!recompute_guarded(_L("Rebuilding model…"))) set_status(StatusKind::Error, wxString::Format(_L("The model could not be rebuilt: %s"), kernel_error_text(m_doc.error))); else @@ -6188,7 +6185,7 @@ static wxString sketch_step_prompt(DesignSketchTool::Mode m, int step, int picks return picks > 0 ? wxString::Format(_L("%d selected · Del removes them · Shift-click adds · " "double-click takes the whole loop"), picks) - : _L("Select — click an entity to pick it · drag an endpoint or centre to move it · " + : _L("Select — click an entity to pick it · drag an endpoint or center to move it · " "Shift-click adds · Del removes"); case Mode::Constrain: return picks > 0 @@ -6209,8 +6206,8 @@ static wxString sketch_step_prompt(DesignSketchTool::Mode m, int step, int picks return step == 0 ? _L("Rectangle — click one corner") : _L("Rectangle — click the opposite corner"); case Mode::CenterRect: - return step == 0 ? _L("Centre rectangle — click the centre") - : _L("Centre rectangle — click a corner"); + return step == 0 ? _L("Center rectangle — click the center") + : _L("Center rectangle — click a corner"); case Mode::ObliqueRect: return step == 0 ? _L("Oblique rectangle — click the start of the base edge") : step == 1 ? _L("Oblique rectangle — click the end of the base edge (this sets the angle)") @@ -6220,7 +6217,7 @@ static wxString sketch_step_prompt(DesignSketchTool::Mode m, int step, int picks : step == 1 ? _L("Rounded rectangle — click the opposite corner") : _L("Rounded rectangle — click to set the corner radius"); case Mode::CenterCircle: - return step == 0 ? _L("Circle — click the centre") + return step == 0 ? _L("Circle — click the center") : _L("Circle — click to set the radius; it can then be typed"); case Mode::TwoPointCircle: return step == 0 ? _L("Circle (2 points) — click one end of the diameter") @@ -6237,27 +6234,27 @@ static wxString sketch_step_prompt(DesignSketchTool::Mode m, int step, int picks return step == 0 ? _L("Tangent arc — click the endpoint it leaves from") : _L("Tangent arc — click its far end"); case Mode::CenterArc: - return step == 0 ? _L("Centre arc — click the centre") - : step == 1 ? _L("Centre arc — click the start point (this sets the radius)") - : _L("Centre arc — click the end point"); + return step == 0 ? _L("Center arc — click the center") + : step == 1 ? _L("Center arc — click the start point (this sets the radius)") + : _L("Center arc — click the end point"); case Mode::Slot: - return step == 0 ? _L("Slot — click one end of the centreline") - : step == 1 ? _L("Slot — click the other end of the centreline") + return step == 0 ? _L("Slot — click one end of the centerline") + : step == 1 ? _L("Slot — click the other end of the centerline") : _L("Slot — click to set the width"); case Mode::ArcSlot: - return step == 0 ? _L("Arc slot — click the centre the slot curves about") - : step == 1 ? _L("Arc slot — click the start of the centreline (this sets the radius)") - : step == 2 ? _L("Arc slot — click the end of the centreline") + return step == 0 ? _L("Arc slot — click the center the slot curves about") + : step == 1 ? _L("Arc slot — click the start of the centerline (this sets the radius)") + : step == 2 ? _L("Arc slot — click the end of the centerline") : _L("Arc slot — click to set the width"); case Mode::Polygon: - return step == 0 ? _L("Polygon — click the centre") + return step == 0 ? _L("Polygon — click the center") : _L("Polygon — click a vertex (this sets size and orientation)"); case Mode::Ellipse: - return step == 0 ? _L("Ellipse — click the centre") + return step == 0 ? _L("Ellipse — click the center") : step == 1 ? _L("Ellipse — click the end of the major axis") : _L("Ellipse — click a point on the minor axis"); case Mode::EllipseArc: - return step == 0 ? _L("Elliptical arc — click the centre") + return step == 0 ? _L("Elliptical arc — click the center") : step == 1 ? _L("Elliptical arc — click the end of the major axis") : step == 2 ? _L("Elliptical arc — click a point on the minor axis") : step == 3 ? _L("Elliptical arc — click where the arc starts") @@ -6317,7 +6314,7 @@ static wxString sketch_step_prompt(DesignSketchTool::Mode m, int step, int picks : pick_more(_L("Polar array — drag the handle to set the sweep, click the " "count to type it · Enter applies"), picks); case Mode::TransformArt: - return _L("Drag a corner to scale, the centre to move · Enter or ✓ when done, Esc to discard"); + return _L("Drag a corner to scale, the center to move · Enter or ✓ when done, Esc to discard"); } return wxString(); } @@ -6459,7 +6456,8 @@ void DesignPanel::show_offer_menu(const wxPoint& screen_pos) why ? why : "no verb accepts this selection"); menu.Append(base + int(bound.size()), s)->Enable(false); bound.push_back(nullptr); - } else if (live.size() == 1) { + } else if (live.size() == 1 && std::none_of(family.begin(), family.end(), [&](const OfferVerb* v) { + return (v->accepts & bit) && !applies(*v); })) { offer_trace("row=%d %s -> %s%s", row, kOfferRowNames[row], live[0]->id, live[0]->action ? "" : " (no GUI route)"); append_offer_item(&menu, base + int(bound.size()), label(*live[0]), *live[0]) @@ -6470,9 +6468,15 @@ void DesignPanel::show_offer_menu(const wxPoint& screen_pos) // (Rectangle -> corner / centre / oblique / rounded); one without sits directly in // the row. Families keep the order of their first member, so the row's layout is // stable across selections — the whole point of a fixed address. + // A verb that is about THIS selection but blocked by the document (no body yet, no + // second sketch) stays in its place, greyed, with its reason (charter 4.1: disabled + // in place, never removed). Verbs that do not accept this selection at all are not + // about it and stay out. auto* sub = new wxMenu(); std::vector> groups; // insertion-ordered - for (const OfferVerb* v : live) { + for (const OfferVerb* v : family) { + const bool ok = applies(*v); + if (!ok && !(v->accepts & bit)) continue; wxMenu* target = sub; if (v->family && *v->family) { auto it = std::find_if(groups.begin(), groups.end(), @@ -6486,6 +6490,14 @@ void DesignPanel::show_offer_menu(const wxPoint& screen_pos) target = it->second; } } + if (!ok) { + offer_trace("row=%d %s ~ %s BLOCKED", row, kOfferRowNames[row], v->id); + wxString s = tr(v->name); + if (v->refusal) s += wxString::FromUTF8(" — ") + tr(v->refusal); + append_offer_item(target, base + int(bound.size()), s, *v)->Enable(false); + bound.push_back(nullptr); + continue; + } offer_trace("row=%d %s > %s%s%s%s", row, kOfferRowNames[row], (v->family && *v->family) ? v->family : "", (v->family && *v->family) ? " > " : "", v->id, @@ -6615,7 +6627,7 @@ void DesignPanel::show_offer_menu(const wxPoint& screen_pos) const int i = e.GetMenuId() - base; if (i < 0 || i >= int(bound.size()) || bound[i] == nullptr || bound[i]->hint == nullptr) return; - set_status(StatusKind::Info, wxGetTranslation(wxString::FromUTF8(bound[i]->hint))); + set_status(StatusKind::Info, wxGetTranslation(wxString::FromUTF8(bound[i]->hint), SLIC3R_APP_KEY)); m_status->Update(); // the popup owns the loop; without this the line repaints late }, base, base + 499); // 499: the mate section starts at base + 500 (see mate_base) menu.Bind(wxEVT_MENU, [this, &bound](wxCommandEvent& e) { @@ -6817,7 +6829,7 @@ bool DesignPanel::on_add_plane() m_feature_counter++; int idx = m_doc.add_plane(m_plane_base->GetSelection(), m_plane_offset->GetValue(), m_plane_tilt->GetValue(), m_plane_tilt_axis->GetSelection(), - "Plane" + std::to_string(m_feature_counter)); + feature_name(_L("Plane"))); if (idx >= 0 && idx < int(m_doc.features.size())) apply_plane_refs(m_doc.features[idx]); m_doc.recompute(); // datum-only docs yield no body; that is expected/benign set_status(StatusKind::Info, _L("Plane added — pick it as a sketch plane")); @@ -6829,7 +6841,7 @@ void DesignPanel::on_add_axis() { m_feature_counter++; int idx = m_doc.add_axis((AxisType)m_axis_type->GetSelection(), - "Axis" + std::to_string(m_feature_counter)); + feature_name(_L("Axis"))); if (idx >= 0 && idx < int(m_doc.features.size())) apply_axis_refs(m_doc.features[idx]); m_doc.recompute(); set_status(StatusKind::Info, _L("Axis added")); @@ -6841,24 +6853,24 @@ void DesignPanel::on_add_coordsys() m_feature_counter++; Vec3d pt(m_cs_x->GetValue(), m_cs_y->GetValue(), m_cs_z->GetValue()); int idx = m_doc.add_coordsys((CoordSysType)m_coordsys_type->GetSelection(), pt, - "Coord" + std::to_string(m_feature_counter)); + feature_name(_L("Coordinate system"))); if (idx >= 0 && idx < int(m_doc.features.size())) apply_coordsys_refs(m_doc.features[idx]); m_doc.recompute(); - set_status(StatusKind::Info, _L("Coord Sys added")); + set_status(StatusKind::Info, _L("Coordinate system added")); refresh_tree(); } void DesignPanel::on_add_shell() { if (m_doc.body.IsNull()) { - set_status(_L("Shell needs a solid body — add or import one first")); + set_status(StatusKind::Warning, _L("Shell needs a solid body — add or import one first")); return; } const int face = (m_sel_solid_face >= 0) ? m_sel_solid_face : -1; m_feature_counter++; m_doc.add_shell(m_shell_thickness->GetValue(), face, m_sel_solid_body, - "Shell" + std::to_string(m_feature_counter)); + feature_name(_L("Shell"))); if (!recompute_guarded(_L("Rebuilding model…"))) set_status(StatusKind::Error, wxString::Format(_L("The model could not be rebuilt: %s"), kernel_error_text(m_doc.error))); @@ -6871,7 +6883,7 @@ void DesignPanel::on_add_shell() void DesignPanel::on_add_draft() { if (m_doc.body.IsNull()) { - set_status(_L("Draft needs a solid body — add or import one first")); + set_status(StatusKind::Warning, _L("Draft needs a solid body — add or import one first")); return; } if (m_sel_solid_face < 0) { @@ -6881,7 +6893,7 @@ void DesignPanel::on_add_draft() m_feature_counter++; m_doc.add_draft(m_draft_angle->GetValue(), m_sel_solid_face, m_sel_solid_body, - "Draft" + std::to_string(m_feature_counter)); + feature_name(_L("Draft"))); if (!recompute_guarded(_L("Rebuilding model…"))) set_status(StatusKind::Error, wxString::Format(_L("The model could not be rebuilt: %s"), kernel_error_text(m_doc.error))); @@ -7000,6 +7012,10 @@ void DesignPanel::hydrate_from_model() if (!m_doc.origin_from_recipe) { const Vec2d bc = plater->build_volume().bed_center(); m_doc.modeling_origin = Vec3d(bc.x(), bc.y(), 0.0); + // A document started here takes the weld rule from the preference; a loaded one + // brings its own (load_recipe). + m_doc.auto_close_loops = wxGetApp().is_auto_close_sketch_loops(); + Slic3r::set_sketch_auto_close(m_doc.auto_close_loops); } const std::string& blob = plater->model().cad_recipe; if (!blob.empty()) load_recipe(blob); @@ -7280,7 +7296,7 @@ void DesignPanel::feed_bodies() void DesignPanel::on_boolean_tool() { if (m_doc.bodies.size() < 2) { - set_status(StatusKind::Error, _L("Boolean needs two bodies — create or import a second solid")); + set_status(StatusKind::Warning, _L("Boolean needs two bodies — add or import a second one")); return; } populate_body_choices(); @@ -7381,7 +7397,7 @@ void DesignPanel::on_set_body_color() (unsigned char)picked.Blue(), (unsigned char)255); feed_bodies(); // same refresh path the visibility toggle uses → viewport updates immediately - set_status(StatusKind::Info, wxString::Format(_L("Body %d colour set"), b + 1)); + set_status(StatusKind::Info, wxString::Format(_L("Body %d color set"), b + 1)); } // Prepare's "Place on Face" (F), ported to Design. Pick a body face, then this rotates the @@ -7485,6 +7501,8 @@ void DesignPanel::clear_document() { tool_cancel(); // leave any active tool / sketch / constrain cleanly m_doc.clear(); // features + bodies + meshes + history + m_doc.auto_close_loops = wxGetApp().is_auto_close_sketch_loops(); // a new design: today's preference + Slic3r::set_sketch_auto_close(m_doc.auto_close_loops); m_edit_index = -1; m_move_body = -1; show_move_card(false); @@ -7498,8 +7516,9 @@ void DesignPanel::clear_document() // The verb the offer names when you point at a body, or at any face/edge/vertex of one. A body // is a recomputed RESULT, so what actually gets deleted is the feature that created it // (CadBody::source_feature). That is an edit to the recipe and can take other features with it, -// so it asks first and NAMES the feature: a body disappearing from the viewport is not by itself -// evidence of which feature went, and this is the one action here that cannot be eyeballed. +// so the status line NAMES the feature that went: a body disappearing from the viewport is not by +// itself evidence of which feature it was. No confirmation — it is one Ctrl+Z away, like every +// other delete here, and a modal in the modelling loop is the thing charter 4.2 removes. void DesignPanel::on_delete_body() { const int nb = int(m_doc.bodies.size()); @@ -7517,11 +7536,6 @@ void DesignPanel::on_delete_body() const std::string& raw = m_doc.features[src].name; const wxString fname = raw.empty() ? wxString::Format(_L("feature %d"), src + 1) : wxString::FromUTF8(raw); - if (wxMessageBox(wxString::Format( - _L("Delete %s?\n\nThat is the feature this body was made from. " - "Features built on it may be removed or stop working."), fname), - _L("Delete body"), wxYES_NO | wxNO_DEFAULT | wxICON_QUESTION, this) != wxYES) - return; // A card left open over a feature that is about to vanish goes stale — same reason // on_delete_feature() closes it. if (m_active != Tool::None || m_edit_index >= 0) { @@ -7531,7 +7545,10 @@ void DesignPanel::on_delete_body() m_doc.checkpoint(); // undo boundary: deleting a body's feature m_sel_solid_body = m_sel_solid_face = m_sel_solid_edge = -1; // the selection is about to m_sel_solid_vertex = false; // name a body that is gone - after_tree_edit(m_doc.remove_feature(src)); + const bool ok = m_doc.remove_feature(src); + after_tree_edit(ok); + if (ok) + set_status(StatusKind::Ok, wxString::Format(_L("Deleted %s, the feature this body was made from — Ctrl+Z restores it"), fname)); } void DesignPanel::on_delete_feature() @@ -7544,8 +7561,7 @@ void DesignPanel::on_delete_feature() } int sel = tree_selection(); if (sel == wxNOT_FOUND) { - set_status(_L("Select a feature in the tree first")); - m_status->Refresh(); + set_status(StatusKind::Info, _L("Select a feature in the tree first")); return; } // If a feature dialog is open (e.g. the feature is being edited), dismiss it first — @@ -7616,8 +7632,7 @@ void DesignPanel::on_move_feature(int delta) { int sel = tree_selection(); if (sel == wxNOT_FOUND) { - set_status(_L("Select a feature in the tree first")); - m_status->Refresh(); + set_status(StatusKind::Info, _L("Select a feature in the tree first")); return; } int target = sel + delta; @@ -9246,8 +9261,7 @@ void DesignPanel::on_edit_feature() { int sel = tree_selection(); if (sel == wxNOT_FOUND) { - set_status(_L("Select a feature in the tree first")); - m_status->Refresh(); + set_status(StatusKind::Info, _L("Select a feature in the tree first")); return; } const CadFeature& f = m_doc.features[sel]; @@ -9859,7 +9873,8 @@ CadFeature DesignPanel::build_candidate(Tool t) const && m_active != Tool::DeleteFace && m_active != Tool::Rib && m_active != Tool::Project && m_active != Tool::Helix && m_active != Tool::SurfaceOffset && m_active != Tool::ThickenSurface) - f.target_body = m_sel_solid_body; + f.target_body = (m_active == Tool::Extrude && m_sel_solid_body < 0) ? m_extrude_auto_body + : m_sel_solid_body; return f; } @@ -10520,7 +10535,7 @@ void DesignPanel::refresh_preview() switch (m_active) { case Tool::Plane: ready = _L("Plane ready"); break; case Tool::Axis: ready = _L("Axis ready"); break; - case Tool::CoordSys: ready = _L("Coord Sys ready"); break; + case Tool::CoordSys: ready = _L("Coordinate system ready"); break; case Tool::Helix: ready = _L("Helix ready"); break; case Tool::Project: ready = _L("Project ready"); break; default: ready = _L("Sketch ready"); break; @@ -10549,11 +10564,11 @@ void DesignPanel::refresh_preview() if (!has_two) { m_viewport->clear_preview(); m_viewport->set_body_hidden(false); // the ghost replaced them; give them back - set_status(StatusKind::Error, _L("Mate needs at least two CoordSys features")); + set_status(StatusKind::Warning, _L("Mate needs two coordinate systems — create them first")); } else if (same) { m_viewport->clear_preview(); m_viewport->set_body_hidden(false); - set_status(StatusKind::Error, _L("Mate: CS A and CS B must be different")); + set_status(StatusKind::Error, _L("Mate: A and B must be different coordinate systems")); } else { sync_body_xform(); // same reason as the solid path below: drop stale per-body poses const int cs_a = int(reinterpret_cast(m_mate_cs_a->GetClientData(sel_a))); @@ -10880,10 +10895,12 @@ void DesignPanel::open_tool(Tool t) else if (m_extrude_sketch_ref >= 0 && m_extrude_sketch_ref < int(m_doc.features.size())) m_extrude_sketch_label->SetLabel(_L("Sketch: ") + wxString::FromUTF8(m_doc.features[m_extrude_sketch_ref].name)); - // A fresh extrude defaults to New body — even when other bodies exist — so - // overlapping extrudes stay SEPARATE solids instead of silently fusing. Joining - // is opt-in (pick "Join"). Engraving art onto a face still defaults to Cut. - // (Edit-mode keeps the feature's stored mode, set below.) + // The default is inferred from where the profile is (charter L6: most extrudes join). + // A profile drawn on or touching a solid joins THAT body; a pushed/pulled face joins + // its own body; a profile in free space is a New body, so overlapping extrudes are + // never silently fused into something they do not touch. Engraving art onto a face + // still defaults to Cut. (Edit-mode keeps the feature's stored mode, set below.) + m_extrude_auto_body = -1; if (m_edit_index < 0) { const bool on_face_import = m_extrude_sketch_ref >= 0 && m_extrude_sketch_ref < int(m_doc.features.size()) @@ -10891,8 +10908,11 @@ void DesignPanel::open_tool(Tool t) if (on_face_import) { m_mode->SetSelection(2); // Cut — engrave into the face m_flip->SetValue(true); // extrude inward (the face normal points out) + } else if (m_extrude_face_src >= 0) { + m_mode->SetSelection(1); // Join — push/pull grows the face's own body } else { - m_mode->SetSelection(0); // New body (was: Add when a body already existed) + m_extrude_auto_body = m_doc.body_touching_sketch(m_extrude_sketch_ref); + m_mode->SetSelection(m_extrude_auto_body >= 0 ? 1 : 0); // Join : New body } } } @@ -10921,7 +10941,7 @@ void DesignPanel::open_tool(Tool t) case Tool::Boolean: m_hdr_boolean->SetLabel(title(_L("Boolean"))); break; case Tool::Cut: m_hdr_cut->SetLabel(title(_L("Cut"))); break; case Tool::Axis: m_hdr_axis->SetLabel(title(_L("Axis"))); break; - case Tool::CoordSys: m_hdr_coordsys->SetLabel(title(_L("Coord Sys"))); break; + case Tool::CoordSys: m_hdr_coordsys->SetLabel(title(_L("Coordinate system"))); break; case Tool::SurfaceExtrude: m_hdr_surf_extrude->SetLabel(title(_L("Surface Extrude"))); break; case Tool::SurfaceRevolve: m_hdr_surf_revolve->SetLabel(title(_L("Surface Revolve"))); break; case Tool::SurfaceLoft: m_hdr_surf_loft->SetLabel(title(_L("Surface Loft"))); break; diff --git a/src/slic3r/GUI/CAD/DesignPanel.hpp b/src/slic3r/GUI/CAD/DesignPanel.hpp index 34de9258aa..b7161fdb3a 100644 --- a/src/slic3r/GUI/CAD/DesignPanel.hpp +++ b/src/slic3r/GUI/CAD/DesignPanel.hpp @@ -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}; diff --git a/src/slic3r/GUI/CAD/DesignSketchTool.cpp b/src/slic3r/GUI/CAD/DesignSketchTool.cpp index 376392c23e..996a77c1a1 100644 --- a/src/slic3r/GUI/CAD/DesignSketchTool.cpp +++ b/src/slic3r/GUI/CAD/DesignSketchTool.cpp @@ -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& 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); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPrimitive.cpp b/src/slic3r/GUI/Gizmos/GLGizmoPrimitive.cpp index 4e1a197a39..a72b19bb75 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoPrimitive.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoPrimitive.cpp @@ -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(); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoPrimitive.hpp b/src/slic3r/GUI/Gizmos/GLGizmoPrimitive.hpp index f78289a58d..0ead6ae5bb 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoPrimitive.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoPrimitive.hpp @@ -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; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSketch.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSketch.cpp index 195ceba713..b878daa1e2 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSketch.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSketch.cpp @@ -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 -#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()); } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSketch.hpp b/src/slic3r/GUI/Gizmos/GLGizmoSketch.hpp index 5a4b59edfd..22c90f4929 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSketch.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSketch.hpp @@ -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; diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 51fdba9bf8..e60f23fda5 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -10074,10 +10074,23 @@ std::vector Plater::priv::load_files(const std::vector& 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) {} } } diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index d9aedec79b..806d5d86ff 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -9,7 +9,6 @@ #include "I18N.hpp" #include "libslic3r/AppConfig.hpp" #include "libslic3r/Format/DRC.hpp" -#include "libslic3r/CAD/SketchEngine.hpp" #include #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 ButtonDragActions = {_L("None"), _L("Pan"), _L("Rotate")}; diff --git a/tests/libslic3r/test_caddocument.cpp b/tests/libslic3r/test_caddocument.cpp index fed30c1e86..6b3327ee1d 100644 --- a/tests/libslic3r/test_caddocument.cpp +++ b/tests/libslic3r/test_caddocument.cpp @@ -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 +}