diff --git a/docs/CAD/design/SKETCHER_TOOL_ARCHITECTURE.md b/docs/CAD/design/SKETCHER_TOOL_ARCHITECTURE.md
index a9659ac4b0..b450835daa 100644
--- a/docs/CAD/design/SKETCHER_TOOL_ARCHITECTURE.md
+++ b/docs/CAD/design/SKETCHER_TOOL_ARCHITECTURE.md
@@ -262,9 +262,9 @@ and a band is not a "gesture" in the `CadLevel::Gesture` sense because nothing h
|---|---|---|---|---|---|
| `Move` | — | `update_hover` / `update_solid_hover` (**non-consuming**, returns false) | — | passthrough | hover only asks for a repaint, `:10022-10025` |
| `LeftDown` | orbit may begin | latch press, **return false** | — | consume-or-orbit is the canvas's call | `:10058-10063`; consuming here killed orbit once already |
-| `LeftDrag` > 8 px | — | start + drive band, **consume** | — | no longer orbits in this canvas | middle-drag orbits, right-drag pans (`:10013-10016`) |
+| `LeftDrag` > 8 px | — | start + drive band, **consume**, when Shift is held or the left button has no camera action | — | otherwise the left button's drag action (Preferences > Control) | the band is Prepare's Shift+left-drag rectangle selection |
| `LeftUp` | — | commit pick **or** resolve band | — | — | `:10035-10041` |
-| `MiddleDrag` / `RightDrag` | — | must not see it | — | orbit / pan | camera gestures never reach the FSM |
+| `MiddleDrag` / `RightDrag` | — | must not see it | — | the button's drag action (Preferences > Control) | camera gestures never reach the FSM; a right press reaches the tool only once its release shows it was a click |
| `RightClick` | — | — | offer menu | — | `snaporca-xmh6` open: a right-click that only clears the sketch selection eats the offer |
| `Esc` | — | — | `escape()` ladder | — | one route whatever holds focus (`:4228-4231`) |
| `Del` / `Backspace` | — | `delete_selected_or_last_sketch_entity()` | char hook | — | `:4260` |
diff --git a/docs/HLSD/design-tab.md b/docs/HLSD/design-tab.md
index c4bb9fd297..9b8010ce2c 100644
--- a/docs/HLSD/design-tab.md
+++ b/docs/HLSD/design-tab.md
@@ -135,7 +135,7 @@ contract between them is stated in code rather than spread across handlers.
| --- | --- | --- |
| `Transient` | a value field or a popup menu | closes it; the tool stays armed |
| `Gesture` | an uncommitted delta — an entity being drawn, a body being dragged | reverts it; committed work is untouched |
-| `Tool` | a feature card, an armed sketch tool, a constrain session | exits it; drawn entities survive |
+| `Tool` | a feature card, Sketch waiting for its plane, an armed sketch tool, a constrain session | exits it; drawn entities survive |
| `Idle` | nothing transient | clears the selection, a Feature tree or Bodies row included; leaves a sketch session only if it is empty |
`cad_escape_level()` is a `constexpr` free function over a POD of four booleans rather than a
@@ -149,26 +149,66 @@ explicit selection, the sketch ribbon's Cancel, which asks first, or `Ctrl+Z`. A
*session* is deliberately not a `Tool` level; it is the environment the `Idle` level lives in,
which makes the destructive path unrepresentable rather than merely unlikely.
+A body Move is the one `Gesture` that outlives the press: its gizmo stays up between drags until
+Confirm keeps the placement or `Esc` or Cancel puts the body back. Until then the selection is
+held — a click off the gizmo only steers the camera — and undo is refused, since the placement
+is not in the history. Anything that starts another edit (a feature card, a sketch, placing
+imported art or text, another body's Move, a rebuild) keeps the placement, as switching gizmos
+keeps a move in Prepare. The panel ends the Move in one place (`DesignPanel::end_body_move`), so
+the gizmo, the Move / Rotate card and the ✓/✗ cannot outlive one another.
+
Right-click is read at button-up against one budget, 3 px of drift, applied to the whole press
rather than to its end points: a press that wandered past the budget at any moment is
navigation, even if it comes back to where it started, which is what stops a slow, careful
orbit from ending in a menu. There is no time budget — a gesture that means something different
when it is slow is exactly what the interaction charter rules out. The raycast uses the press
-position, not the release. An armed sketch tool that already consumed the right
-button (to terminate a chain, say) declines to also open a menu, through a read-and-clear flag.
-Past either budget the event is navigation, and navigation does not transition the state
-machine.
+position, not the release. The sketch tool sees a right press only once the release has shown it
+was a click: the press itself goes to the camera, which may pan or orbit with that button, and the
+canvas replays it to the tool on a stationary release. A tool that uses the click (to terminate a
+chain, say) keeps the menu closed. Past either budget the event is navigation, and navigation
+does not transition the state machine.
Navigation itself is Prepare's: the camera reads the drag actions set in Preferences > Control
-for each button. The left button is shared with picking, so a whole body is swept with a
-rectangle on plain left-drag only while no camera action is assigned to it, and with
-Shift+left-drag otherwise — Prepare's own rectangle selection.
+for each button, and in the Touchpad camera style a move with Alt held orbits and one with Shift
+held pans, whatever tool is armed. The left button is shared with picking and drawing, so a tool
+handle or a press that draws takes it first, as a gizmo does in Prepare; a whole body is swept
+with a rectangle on plain left-drag only while no camera action is assigned to the left button,
+and with Shift+left-drag otherwise — Prepare's own rectangle selection.
-Entering a sketch changes three things at once so the mode is legible: a banner above the
+Entering a sketch changes two things at once so the mode is legible: a banner above the
canvas (a sibling of the canvas, not a child over it — on GTK a child window over a
-`wxGLCanvas` is a native window and does not reliably stack over GL), the printer bed muted so
-a plate grid is never read as a sketch grid, and `N` to look normal to the plane. Code that
-changes any of the three belongs with a change to this section.
+`wxGLCanvas` is a native window and does not reliably stack over GL), and `N` to look normal to
+the plane. The printer bed stays: there is no sketch grid, so the plate grid is the only ground
+reference a sketch has. Code that changes either belongs with a change to this section.
+
+Sketch mode is never entered without a plane under it, so the banner, the sketch keys and the
+sketch offer always have a session to act on. Sketch on a picked flat face or reference plane opens
+the session on it at once. With nothing picked it stays in Feature mode and waits for one — an
+armed `Tool`, left with `Esc` or ✗, and ended by anything that starts another edit — and the
+reference plane or flat face clicked next opens the session. A picked plane is a selection like a
+face: the sketch on it uses it up, and `Esc` or a click on nothing lets go of it, so a plane that
+can no longer be seen never decides where the next sketch goes.
+
+The reference planes — XY, XZ and YZ through the modeling origin, with their half-axes — are
+drawn on demand, because three translucent squares over every model are noise once they are not
+the thing being picked. Sketch brings them up while it waits for a plane, which is exactly when
+they are picked, and the session the pick opens takes them away; a live session draws none. The
+Feature tree's Origin row keeps them up outside a sketch. Its state is a view preference in
+AppConfig rather than part of the recipe, so it costs the project format nothing. The Plane tool
+keeps its own rule: the planes and the datums as Offset bases, and nothing for the other methods,
+where a click on a plane would rewrite the datum's references. The `P` and `A` keys are a
+separate, unpickable view helper and do not follow the Origin row.
+
+The Bed row, under the Origin row, is the printer bed's switch in the same way: it draws or hides
+the bed and its plate grid in every mode. It is a view preference in AppConfig too, and the bed is
+shown until it is turned off.
+
+The two rows are view switches, not history, and the tree says so: they sit unframed on the Feature
+tree's card, above the features' own framed list, and stay put while the features scroll. A click
+never selects either row, since a selected Origin or Bed would have nothing to edit, move or
+delete; the eye and the right-click menu are the only targets, and a row's label dims while its
+thing is hidden, as a hidden body's does. Because the block never takes the focus, `Ctrl+Shift+O`
+and `Ctrl+Shift+B` flip the Origin and the Bed from the keyboard.
## Rendering the bodies
@@ -189,6 +229,15 @@ degenerate edges are left out (`GeometryEngine::display_edges`), and the polylin
once per shape, keyed by its `TShape`, because a recompute that leaves a body unchanged is the
common case.
+While a feature card is open, its preview ghost is the whole model the candidate would produce,
+drawn translucent over the bodies, so every face the feature leaves alone is in both at the same
+depth. The ghost is drawn with a depth bias that pushes it back (`GLVolume::depth_bias`), so on a shared face
+the body always wins instead of the two copies z-fighting, and the ghost shows only where the
+result reaches past the bodies. Material a feature removes lies inside the old solid and would not
+show at all, so the tools whose result mostly coincides with the body — Fillet/Chamfer, Draft,
+Hole and the Mate hover — hide the bodies once the preview is valid and draw the result alone,
+opaque.
+
## Showing what is selected
A selection is drawn on the faces it names, never as a tint over the body: a translucent
@@ -214,7 +263,17 @@ replay costs up to a recompute, so the panel finds the faces once per row and to
generation, off the UI thread, and only while no feature card is open. One selection is live at
a time: a viewport pick clears the feature row and a feature row clears the viewport pick, as the
Feature tree and Bodies list do between themselves. `Esc`, a click on empty space and an
-empty rubber band all let go of it, whichever list or pick made it.
+empty rubber band all let go of it, whichever list or pick made it — except while a body Move is
+open, which holds the selection until it ends (see the interaction contract).
+
+Zoom to selection, on a Feature tree or Bodies row and in its right-click menu, frames one thing
+along the current view direction, as the canvas's Fit button frames the selection. On a body it
+frames the body whole, hidden or not, from its display mesh: the viewport never selects a hidden
+body, so the selection cannot stand in for it. On a sketch it frames the sketch's own geometry,
+drawn, consumed or suppressed; on any other feature, the faces the feature made, found as the row
+highlight finds them and from the same cache. From the offer it frames whatever the selection is,
+a body again included. A feature that is not a sketch and makes no faces, such as a datum plane
+or a suppressed Extrude, has no Zoom to selection.
## Following the app
@@ -231,9 +290,21 @@ The tab is a page of Orca's main window and answers to the same settings as Prep
- **Sidebar icons.** Every clickable icon in the sidebar shows a hover chip. The card-header and
constraint-row buttons are Orca's self-painted `Button`, because a native button cannot take a
hover background on macOS. The Feature tree and Bodies lists are a custom-drawn
- `DesignRowList` rather than a `wxTreeCtrl`, so each row carries its own actions — Edit,
- Show/hide and Delete on a feature, Move, Show/hide and Delete on a body — and the eye shows
- whether that row is hidden.
+ `DesignRowList` rather than a `wxTreeCtrl`, so each row carries its own actions — Zoom to
+ selection, Edit, Show/hide and Delete on a feature, Zoom to selection, Move, Show/hide and
+ Delete on a body, and only Show/hide on the Origin and Bed rows, a separate non-selectable list
+ above the features — and the eye shows whether that row is hidden. A feature with nothing to
+ frame keeps a blank cell where Zoom to selection would be, so every icon stays in its column.
+- **Plates.** This is the one thing the tab does not follow. The canvas has a bed of its own at
+ the printer bed's home position, whichever plate Prepare has current, and a new document's
+ modeling origin is that bed's centre. A bed that followed the current plate would slide out
+ from under a design: the origin is fixed once per document, baked into every sketch plane and
+ saved in the recipe, while the current plate can change between visits. Commit to Plate does
+ not need it either, since the committed object is placed on an empty spot of the current
+ plate. What the canvas does read from the plate is moved onto its bed: the exclude areas, the
+ plate box the camera orbits about when nothing is picked (`GLCanvas3D::_current_plate_box`),
+ and the first view, which starts from Prepare's camera turned to the tab's iso view: the CAD
+ isometric from the front-right corner that Home returns to, not Prepare's front-left one.
- **Viewport text.** The status line and the active tool's values are drawn by the canvas in
its ImGui pass, so they go with the canvas: a top-level window over GL does not follow its
frame and was left floating over other applications.
@@ -256,12 +327,12 @@ permanent row index, verbs that do not apply shown disabled **in place with thei
rather than removed. The invariant is that a verb's row index is identical in every selection
where it appears and that adding a verb never moves an existing one — the hand learns the
position, so the menu is never re-sorted, compacted or adaptively ordered. Above the families
-sits one *flat* row, holding Rename and Color — what a selection is opened for most: its verbs are
-items of their own at the top of the menu rather than a family's submenu. It is appended after
-the eight, so it moved no existing index, and it reads the same from the viewport and from a row
-of the Bodies list.
+sits one *flat* row, holding Rename, Color and Zoom to selection — what a selection is opened for
+most: its verbs are items of their own at the top of the menu rather than a family's submenu. It
+is appended after the eight, so it moved no existing index, and it reads the same from the
+viewport and from a row of the Bodies list.
-An invariant across 92 verbs and 20 selection kinds does not survive by review, so the map
+An invariant across 93 verbs and 20 selection kinds does not survive by review, so the map
exists once, as data: `scripts/CAD/tool_atlas.json` carries every verb with its row, key, icon,
accepted selections, preconditions and refusal string, and `scripts/CAD/gen_offer_table.py`
emits `src/slic3r/GUI/CAD/DesignOffer.hpp` from it. The header is checked in and never
diff --git a/docs/HLSD/prusa-indx-profiles.md b/docs/HLSD/prusa-indx-profiles.md
index 272e5bb5aa..77a398b2d5 100644
--- a/docs/HLSD/prusa-indx-profiles.md
+++ b/docs/HLSD/prusa-indx-profiles.md
@@ -36,8 +36,13 @@ override is not available in these profiles. The source's `EXCLUDE_E_START` and
`EXCLUDE_E_END` internal markers become comments rather than printer commands.
Pressure-advance restoration and automatic pressure-advance emission use the
-selected filament preset's settings. These profiles do not impose machine-owned
-filament overrides. Dock-fan control retains the source's material and layer
+selected filament preset's settings. The INDX filament presets include
+`fdm_filament_template_indx`, whose start G-code sets the filament's pressure
+advance with `M572` and then starts the firmware's automatic calibration with
+`M573 R`, the commands PrusaSlicer emits after a tool change; the station purge
+disables pressure advance before it. The template also carries Prusa's
+multi-tool ramming and a 10 mm³ minimal purge. These profiles do not impose
+machine-owned filament overrides. Dock-fan control retains the source's material and layer
conditions; shutdown parks the tool and turns off the used heaters and dock fan.
## Configuration boundaries
diff --git a/resources/images/design_bed.svg b/resources/images/design_bed.svg
new file mode 100644
index 0000000000..a9441e7b5d
--- /dev/null
+++ b/resources/images/design_bed.svg
@@ -0,0 +1 @@
+
diff --git a/resources/images/design_export.svg b/resources/images/design_export.svg
new file mode 100644
index 0000000000..b998ea19fc
--- /dev/null
+++ b/resources/images/design_export.svg
@@ -0,0 +1 @@
+
diff --git a/resources/images/design_import_mesh.svg b/resources/images/design_import_mesh.svg
new file mode 100644
index 0000000000..7517578578
--- /dev/null
+++ b/resources/images/design_import_mesh.svg
@@ -0,0 +1 @@
+
diff --git a/resources/images/design_new.svg b/resources/images/design_new.svg
new file mode 100644
index 0000000000..dfe7425de4
--- /dev/null
+++ b/resources/images/design_new.svg
@@ -0,0 +1 @@
+
diff --git a/resources/images/design_new_sketch.svg b/resources/images/design_new_sketch.svg
new file mode 100644
index 0000000000..a645cf1dff
--- /dev/null
+++ b/resources/images/design_new_sketch.svg
@@ -0,0 +1 @@
+
diff --git a/resources/images/design_step.svg b/resources/images/design_step.svg
index a4a92b7b20..45974dfd77 100644
--- a/resources/images/design_step.svg
+++ b/resources/images/design_step.svg
@@ -1 +1 @@
-
+
diff --git a/resources/images/design_zoom.svg b/resources/images/design_zoom.svg
new file mode 100644
index 0000000000..e9048dcff4
--- /dev/null
+++ b/resources/images/design_zoom.svg
@@ -0,0 +1 @@
+
diff --git a/resources/profiles/Prusa.json b/resources/profiles/Prusa.json
index 57884bea9b..1b46c1deff 100644
--- a/resources/profiles/Prusa.json
+++ b/resources/profiles/Prusa.json
@@ -1,6 +1,6 @@
{
"name": "Prusa",
- "version": "02.04.00.15",
+ "version": "02.04.00.16",
"force_update": "0",
"description": "Prusa configurations",
"machine_model_list": [
@@ -1573,48 +1573,32 @@
],
"filament_list": [
{
- "name": "Generic ASA @Prusa CORE One",
- "sub_path": "filament/Generic ASA @Prusa CORE One.json"
- },
- {
- "name": "Generic PLA Silk @Prusa CORE One",
- "sub_path": "filament/Generic PLA Silk @Prusa CORE One.json"
+ "name": "Generic PC @Prusa",
+ "sub_path": "filament/Generic PC @Prusa.json"
},
{
"name": "fdm_filament_common",
"sub_path": "filament/fdm_filament_common.json"
},
{
- "name": "Generic ASA @Prusa CORE One 0.6",
- "sub_path": "filament/Generic ASA @Prusa CORE One 0.6.json"
+ "name": "fdm_filament_template_indx",
+ "sub_path": "filament/fdm_filament_template_indx.json"
},
{
- "name": "Generic ASA @Prusa CORE One 0.8",
- "sub_path": "filament/Generic ASA @Prusa CORE One 0.8.json"
+ "name": "fdm_filament_template_nextruder_pet",
+ "sub_path": "filament/fdm_filament_template_nextruder_pet.json"
},
{
- "name": "Generic ASA @Prusa CORE One HF 0.4",
- "sub_path": "filament/Generic ASA @Prusa CORE One HF 0.4.json"
+ "name": "fdm_filament_template_nextruder_pet_hf",
+ "sub_path": "filament/fdm_filament_template_nextruder_pet_hf.json"
},
{
- "name": "Generic ASA @Prusa CORE One HF 0.5",
- "sub_path": "filament/Generic ASA @Prusa CORE One HF 0.5.json"
+ "name": "fdm_filament_template_nextruder_pla",
+ "sub_path": "filament/fdm_filament_template_nextruder_pla.json"
},
{
- "name": "Generic ASA @Prusa CORE One HF 0.6",
- "sub_path": "filament/Generic ASA @Prusa CORE One HF 0.6.json"
- },
- {
- "name": "Generic ASA @Prusa CORE One HF 0.8",
- "sub_path": "filament/Generic ASA @Prusa CORE One HF 0.8.json"
- },
- {
- "name": "Generic PLA Silk @Prusa CORE One 0.6",
- "sub_path": "filament/Generic PLA Silk @Prusa CORE One 0.6.json"
- },
- {
- "name": "Generic PLA Silk @Prusa CORE One 0.8",
- "sub_path": "filament/Generic PLA Silk @Prusa CORE One 0.8.json"
+ "name": "fdm_filament_template_nextruder_pla_hf",
+ "sub_path": "filament/fdm_filament_template_nextruder_pla_hf.json"
},
{
"name": "fdm_filament_abs",
@@ -1664,6 +1648,22 @@
"name": "fdm_filament_tpu",
"sub_path": "filament/fdm_filament_tpu.json"
},
+ {
+ "name": "Generic PLA Silk @Prusa CORE One INDX",
+ "sub_path": "filament/Generic PLA Silk @Prusa CORE One INDX.json"
+ },
+ {
+ "name": "Generic ASA @Prusa CORE One",
+ "sub_path": "filament/Generic ASA @Prusa CORE One.json"
+ },
+ {
+ "name": "Generic PC @Prusa MK4 MMU3",
+ "sub_path": "filament/Generic PC @Prusa MK4 MMU3.json"
+ },
+ {
+ "name": "Generic PLA Silk @Prusa CORE One",
+ "sub_path": "filament/Generic PLA Silk @Prusa CORE One.json"
+ },
{
"name": "Generic ABS @Prusa MK3.5",
"sub_path": "filament/Generic ABS @Prusa MK3.5.json"
@@ -2160,6 +2160,38 @@
"name": "Generic TPU HF @Prusa MK3.5",
"sub_path": "filament/Generic TPU HF @Prusa MK3.5.json"
},
+ {
+ "name": "Generic ASA @Prusa CORE One 0.6",
+ "sub_path": "filament/Generic ASA @Prusa CORE One 0.6.json"
+ },
+ {
+ "name": "Generic ASA @Prusa CORE One 0.8",
+ "sub_path": "filament/Generic ASA @Prusa CORE One 0.8.json"
+ },
+ {
+ "name": "Generic ASA @Prusa CORE One HF 0.4",
+ "sub_path": "filament/Generic ASA @Prusa CORE One HF 0.4.json"
+ },
+ {
+ "name": "Generic ASA @Prusa CORE One HF 0.5",
+ "sub_path": "filament/Generic ASA @Prusa CORE One HF 0.5.json"
+ },
+ {
+ "name": "Generic ASA @Prusa CORE One HF 0.6",
+ "sub_path": "filament/Generic ASA @Prusa CORE One HF 0.6.json"
+ },
+ {
+ "name": "Generic ASA @Prusa CORE One HF 0.8",
+ "sub_path": "filament/Generic ASA @Prusa CORE One HF 0.8.json"
+ },
+ {
+ "name": "Generic PLA Silk @Prusa CORE One 0.6",
+ "sub_path": "filament/Generic PLA Silk @Prusa CORE One 0.6.json"
+ },
+ {
+ "name": "Generic PLA Silk @Prusa CORE One 0.8",
+ "sub_path": "filament/Generic PLA Silk @Prusa CORE One 0.8.json"
+ },
{
"name": "Generic ABS @Prusa MK4S HF0.4",
"sub_path": "filament/Generic ABS @Prusa MK4S HF0.4.json"
@@ -2212,6 +2244,10 @@
"name": "Generic ABS HF @Prusa MINIIS 0.8",
"sub_path": "filament/Generic ABS HF @Prusa MINIIS 0.8.json"
},
+ {
+ "name": "Generic ASA @Prusa MK4 MMU3",
+ "sub_path": "filament/Generic ASA @Prusa MK4 MMU3.json"
+ },
{
"name": "Generic ASA @Prusa MK4S 0.6",
"sub_path": "filament/Generic ASA @Prusa MK4S 0.6.json"
@@ -2260,6 +2296,10 @@
"name": "Prusament ASA @CORE One HF 0.8",
"sub_path": "filament/Prusament ASA @CORE One HF 0.8.json"
},
+ {
+ "name": "Prusament ASA @MK4 MMU3",
+ "sub_path": "filament/Prusament ASA @MK4 MMU3.json"
+ },
{
"name": "Prusament ASA @MK4S 0.6",
"sub_path": "filament/Prusament ASA @MK4S 0.6.json"
@@ -2392,6 +2432,10 @@
"name": "Prusament PC Blend @CORE One HF 0.8",
"sub_path": "filament/Prusament PC Blend @CORE One HF 0.8.json"
},
+ {
+ "name": "Prusament PC Blend @MK4 MMU3",
+ "sub_path": "filament/Prusament PC Blend @MK4 MMU3.json"
+ },
{
"name": "Prusament PC Blend @MK4S 0.6",
"sub_path": "filament/Prusament PC Blend @MK4S 0.6.json"
@@ -2532,6 +2576,10 @@
"name": "Prusament PETG @CORE One HF 0.8",
"sub_path": "filament/Prusament PETG @CORE One HF 0.8.json"
},
+ {
+ "name": "Prusament PETG @MK4 MMU3",
+ "sub_path": "filament/Prusament PETG @MK4 MMU3.json"
+ },
{
"name": "Prusament PETG @MK4S 0.6",
"sub_path": "filament/Prusament PETG @MK4S 0.6.json"
@@ -2668,6 +2716,10 @@
"name": "Prusament PLA @CORE One HF 0.8",
"sub_path": "filament/Prusament PLA @CORE One HF 0.8.json"
},
+ {
+ "name": "Prusament PLA @MK4 MMU3",
+ "sub_path": "filament/Prusament PLA @MK4 MMU3.json"
+ },
{
"name": "Prusament PLA @MK4S 0.6",
"sub_path": "filament/Prusament PLA @MK4S 0.6.json"
@@ -2704,6 +2756,10 @@
"name": "Prusament rPLA @CORE One 0.8",
"sub_path": "filament/Prusament rPLA @CORE One 0.8.json"
},
+ {
+ "name": "Prusament rPLA @MK4 MMU3",
+ "sub_path": "filament/Prusament rPLA @MK4 MMU3.json"
+ },
{
"name": "Prusament rPLA @MK4S 0.6",
"sub_path": "filament/Prusament rPLA @MK4S 0.6.json"
@@ -2856,6 +2912,10 @@
"name": "Generic ABS @Prusa CORE One HF 0.8",
"sub_path": "filament/Generic ABS @Prusa CORE One HF 0.8.json"
},
+ {
+ "name": "Generic ABS @Prusa MK4 MMU3",
+ "sub_path": "filament/Generic ABS @Prusa MK4 MMU3.json"
+ },
{
"name": "Generic ASA @Prusa MK4S HF0.5",
"sub_path": "filament/Generic ASA @Prusa MK4S HF0.5.json"
@@ -2904,6 +2964,10 @@
"name": "Generic PETG @Prusa CORE One HF 0.8",
"sub_path": "filament/Generic PETG @Prusa CORE One HF 0.8.json"
},
+ {
+ "name": "Generic PETG @Prusa MK4 MMU3",
+ "sub_path": "filament/Generic PETG @Prusa MK4 MMU3.json"
+ },
{
"name": "Generic PLA @Prusa MK4S HF0.5",
"sub_path": "filament/Generic PLA @Prusa MK4S HF0.5.json"
@@ -2948,6 +3012,10 @@
"name": "Generic PLA @Prusa CORE One HF 0.8",
"sub_path": "filament/Generic PLA @Prusa CORE One HF 0.8.json"
},
+ {
+ "name": "Generic PLA @Prusa MK4 MMU3",
+ "sub_path": "filament/Generic PLA @Prusa MK4 MMU3.json"
+ },
{
"name": "Generic TPU @Prusa CORE One 0.6",
"sub_path": "filament/Generic TPU @Prusa CORE One 0.6.json"
@@ -2955,14 +3023,6 @@
{
"name": "Generic TPU @Prusa CORE One 0.8",
"sub_path": "filament/Generic TPU @Prusa CORE One 0.8.json"
- },
- {
- "name": "Generic PC @Prusa",
- "sub_path": "filament/Generic PC @Prusa.json"
- },
- {
- "name": "Generic PLA Silk @Prusa CORE One INDX",
- "sub_path": "filament/Generic PLA Silk @Prusa CORE One INDX.json"
}
],
"machine_list": [
diff --git a/resources/profiles/Prusa/filament/Generic ABS @Prusa CORE One HF 0.4.json b/resources/profiles/Prusa/filament/Generic ABS @Prusa CORE One HF 0.4.json
index caf36dc7d1..205704705f 100644
--- a/resources/profiles/Prusa/filament/Generic ABS @Prusa CORE One HF 0.4.json
+++ b/resources/profiles/Prusa/filament/Generic ABS @Prusa CORE One HF 0.4.json
@@ -7,6 +7,9 @@
"setting_id": "ASYz8yASE82tQDII",
"filament_id": "OFY9muEs",
"instantiation": "true",
+ "include": [
+ "fdm_filament_template_nextruder_pet_hf"
+ ],
"compatible_printers": [
"Prusa CORE One HF 0.4 nozzle"
],
diff --git a/resources/profiles/Prusa/filament/Generic ABS @Prusa CORE One HF 0.5.json b/resources/profiles/Prusa/filament/Generic ABS @Prusa CORE One HF 0.5.json
index 4d0ae20a33..631940cee5 100644
--- a/resources/profiles/Prusa/filament/Generic ABS @Prusa CORE One HF 0.5.json
+++ b/resources/profiles/Prusa/filament/Generic ABS @Prusa CORE One HF 0.5.json
@@ -7,6 +7,9 @@
"setting_id": "mzZCm8XDmAR23YR6",
"filament_id": "OFY9muEs",
"instantiation": "true",
+ "include": [
+ "fdm_filament_template_nextruder_pet_hf"
+ ],
"compatible_printers": [
"Prusa CORE One HF 0.5 nozzle"
],
diff --git a/resources/profiles/Prusa/filament/Generic ABS @Prusa CORE One HF 0.6.json b/resources/profiles/Prusa/filament/Generic ABS @Prusa CORE One HF 0.6.json
index 6419e2a9d4..4b0a714f62 100644
--- a/resources/profiles/Prusa/filament/Generic ABS @Prusa CORE One HF 0.6.json
+++ b/resources/profiles/Prusa/filament/Generic ABS @Prusa CORE One HF 0.6.json
@@ -7,6 +7,9 @@
"setting_id": "gJhX2KBpk4wfXIj0",
"filament_id": "OFY9muEs",
"instantiation": "true",
+ "include": [
+ "fdm_filament_template_nextruder_pet_hf"
+ ],
"compatible_printers": [
"Prusa CORE One HF 0.6 nozzle"
],
diff --git a/resources/profiles/Prusa/filament/Generic ABS @Prusa CORE One INDX.json b/resources/profiles/Prusa/filament/Generic ABS @Prusa CORE One INDX.json
index 7a154dfc54..6a4bdbf5fb 100644
--- a/resources/profiles/Prusa/filament/Generic ABS @Prusa CORE One INDX.json
+++ b/resources/profiles/Prusa/filament/Generic ABS @Prusa CORE One INDX.json
@@ -5,6 +5,9 @@
"from": "system",
"setting_id": "JEatvybG9xIkrPk3",
"instantiation": "true",
+ "include": [
+ "fdm_filament_template_indx"
+ ],
"filament_cost": [
"34.65"
],
diff --git a/resources/profiles/Prusa/filament/Generic ABS @Prusa CORE One.json b/resources/profiles/Prusa/filament/Generic ABS @Prusa CORE One.json
index a3fcbdee47..5138e20528 100644
--- a/resources/profiles/Prusa/filament/Generic ABS @Prusa CORE One.json
+++ b/resources/profiles/Prusa/filament/Generic ABS @Prusa CORE One.json
@@ -6,6 +6,9 @@
"from": "system",
"setting_id": "0k31oXq1pLQsHvrj",
"instantiation": "true",
+ "include": [
+ "fdm_filament_template_nextruder_pet"
+ ],
"chamber_temperature": [
"40"
],
diff --git a/resources/profiles/Prusa/filament/Generic ABS @Prusa MK4 MMU3.json b/resources/profiles/Prusa/filament/Generic ABS @Prusa MK4 MMU3.json
new file mode 100644
index 0000000000..9c3a15622a
--- /dev/null
+++ b/resources/profiles/Prusa/filament/Generic ABS @Prusa MK4 MMU3.json
@@ -0,0 +1,19 @@
+{
+ "type": "filament",
+ "name": "Generic ABS @Prusa MK4 MMU3",
+ "inherits": "Generic ABS @Prusa MK4",
+ "from": "system",
+ "setting_id": "uFmixo2FX1yxNsDB",
+ "instantiation": "true",
+ "include": [
+ "fdm_filament_template_nextruder_pet"
+ ],
+ "compatible_printers": [
+ "Prusa MK4 MMU3 0.4 nozzle",
+ "Prusa MK4 MMU3 0.6 nozzle",
+ "Prusa MK4 MMU3 HF0.4 nozzle"
+ ],
+ "filament_minimal_purge_on_wipe_tower": [
+ "35"
+ ]
+}
diff --git a/resources/profiles/Prusa/filament/Generic ASA @Prusa CORE One HF 0.4.json b/resources/profiles/Prusa/filament/Generic ASA @Prusa CORE One HF 0.4.json
index 80c129fab9..c368fcae43 100644
--- a/resources/profiles/Prusa/filament/Generic ASA @Prusa CORE One HF 0.4.json
+++ b/resources/profiles/Prusa/filament/Generic ASA @Prusa CORE One HF 0.4.json
@@ -7,6 +7,9 @@
"setting_id": "eKDfiVRE59itoppm",
"filament_id": "OFLPAxz3",
"instantiation": "true",
+ "include": [
+ "fdm_filament_template_nextruder_pet_hf"
+ ],
"compatible_printers": [
"Prusa CORE One HF 0.4 nozzle"
],
diff --git a/resources/profiles/Prusa/filament/Generic ASA @Prusa CORE One HF 0.5.json b/resources/profiles/Prusa/filament/Generic ASA @Prusa CORE One HF 0.5.json
index 4ea45b0f5b..07ee914c50 100644
--- a/resources/profiles/Prusa/filament/Generic ASA @Prusa CORE One HF 0.5.json
+++ b/resources/profiles/Prusa/filament/Generic ASA @Prusa CORE One HF 0.5.json
@@ -7,6 +7,9 @@
"setting_id": "ZjHf5Vvtf4ESNnWb",
"filament_id": "OFLPAxz3",
"instantiation": "true",
+ "include": [
+ "fdm_filament_template_nextruder_pet_hf"
+ ],
"compatible_printers": [
"Prusa CORE One HF 0.5 nozzle"
],
diff --git a/resources/profiles/Prusa/filament/Generic ASA @Prusa CORE One HF 0.6.json b/resources/profiles/Prusa/filament/Generic ASA @Prusa CORE One HF 0.6.json
index d6b2fff248..d602b9e774 100644
--- a/resources/profiles/Prusa/filament/Generic ASA @Prusa CORE One HF 0.6.json
+++ b/resources/profiles/Prusa/filament/Generic ASA @Prusa CORE One HF 0.6.json
@@ -7,6 +7,9 @@
"setting_id": "aRDgUgQJWdNDJx5c",
"filament_id": "OFLPAxz3",
"instantiation": "true",
+ "include": [
+ "fdm_filament_template_nextruder_pet_hf"
+ ],
"compatible_printers": [
"Prusa CORE One HF 0.6 nozzle"
],
diff --git a/resources/profiles/Prusa/filament/Generic ASA @Prusa CORE One.json b/resources/profiles/Prusa/filament/Generic ASA @Prusa CORE One.json
index 09a8349005..d135ffcbad 100644
--- a/resources/profiles/Prusa/filament/Generic ASA @Prusa CORE One.json
+++ b/resources/profiles/Prusa/filament/Generic ASA @Prusa CORE One.json
@@ -6,6 +6,9 @@
"from": "system",
"setting_id": "Q7fgojkHRZI0floy",
"instantiation": "true",
+ "include": [
+ "fdm_filament_template_nextruder_pet"
+ ],
"chamber_temperature": [
"40"
],
diff --git a/resources/profiles/Prusa/filament/Generic ASA @Prusa MK4 MMU3.json b/resources/profiles/Prusa/filament/Generic ASA @Prusa MK4 MMU3.json
new file mode 100644
index 0000000000..dc9f5f3b0e
--- /dev/null
+++ b/resources/profiles/Prusa/filament/Generic ASA @Prusa MK4 MMU3.json
@@ -0,0 +1,19 @@
+{
+ "type": "filament",
+ "name": "Generic ASA @Prusa MK4 MMU3",
+ "inherits": "Generic ASA @Prusa MK4",
+ "from": "system",
+ "setting_id": "iq9bFkpmAOJ4ae4g",
+ "instantiation": "true",
+ "include": [
+ "fdm_filament_template_nextruder_pet"
+ ],
+ "compatible_printers": [
+ "Prusa MK4 MMU3 0.4 nozzle",
+ "Prusa MK4 MMU3 0.6 nozzle",
+ "Prusa MK4 MMU3 HF0.4 nozzle"
+ ],
+ "filament_minimal_purge_on_wipe_tower": [
+ "35"
+ ]
+}
diff --git a/resources/profiles/Prusa/filament/Generic PC @Prusa MK4 MMU3.json b/resources/profiles/Prusa/filament/Generic PC @Prusa MK4 MMU3.json
new file mode 100644
index 0000000000..9d02147b66
--- /dev/null
+++ b/resources/profiles/Prusa/filament/Generic PC @Prusa MK4 MMU3.json
@@ -0,0 +1,16 @@
+{
+ "type": "filament",
+ "name": "Generic PC @Prusa MK4 MMU3",
+ "inherits": "Generic PC @Prusa",
+ "from": "system",
+ "setting_id": "qMawlK9QPI7DcF8I",
+ "instantiation": "true",
+ "include": [
+ "fdm_filament_template_nextruder_pet"
+ ],
+ "compatible_printers": [
+ "Prusa MK4 MMU3 0.4 nozzle",
+ "Prusa MK4 MMU3 0.6 nozzle",
+ "Prusa MK4 MMU3 HF0.4 nozzle"
+ ]
+}
diff --git a/resources/profiles/Prusa/filament/Generic PETG @Prusa CORE One HF 0.4.json b/resources/profiles/Prusa/filament/Generic PETG @Prusa CORE One HF 0.4.json
index 510e1d2baf..38ab9dc4c2 100644
--- a/resources/profiles/Prusa/filament/Generic PETG @Prusa CORE One HF 0.4.json
+++ b/resources/profiles/Prusa/filament/Generic PETG @Prusa CORE One HF 0.4.json
@@ -7,6 +7,9 @@
"setting_id": "7oUyjvRHLH4bJyX0",
"filament_id": "OFYPdQJh",
"instantiation": "true",
+ "include": [
+ "fdm_filament_template_nextruder_pet_hf"
+ ],
"compatible_printers": [
"Prusa CORE One HF 0.4 nozzle"
],
diff --git a/resources/profiles/Prusa/filament/Generic PETG @Prusa CORE One HF 0.5.json b/resources/profiles/Prusa/filament/Generic PETG @Prusa CORE One HF 0.5.json
index f28f5f10a3..f24f1f8f26 100644
--- a/resources/profiles/Prusa/filament/Generic PETG @Prusa CORE One HF 0.5.json
+++ b/resources/profiles/Prusa/filament/Generic PETG @Prusa CORE One HF 0.5.json
@@ -7,6 +7,9 @@
"setting_id": "6HiIOGURZWwman1l",
"filament_id": "OFYPdQJh",
"instantiation": "true",
+ "include": [
+ "fdm_filament_template_nextruder_pet_hf"
+ ],
"compatible_printers": [
"Prusa CORE One HF 0.5 nozzle"
],
diff --git a/resources/profiles/Prusa/filament/Generic PETG @Prusa CORE One HF 0.6.json b/resources/profiles/Prusa/filament/Generic PETG @Prusa CORE One HF 0.6.json
index 53dd49907d..c78dbe3b1e 100644
--- a/resources/profiles/Prusa/filament/Generic PETG @Prusa CORE One HF 0.6.json
+++ b/resources/profiles/Prusa/filament/Generic PETG @Prusa CORE One HF 0.6.json
@@ -7,6 +7,9 @@
"setting_id": "KKiug7A9NOQPe1m7",
"filament_id": "OFYPdQJh",
"instantiation": "true",
+ "include": [
+ "fdm_filament_template_nextruder_pet_hf"
+ ],
"compatible_printers": [
"Prusa CORE One HF 0.6 nozzle"
],
diff --git a/resources/profiles/Prusa/filament/Generic PETG @Prusa CORE One INDX.json b/resources/profiles/Prusa/filament/Generic PETG @Prusa CORE One INDX.json
index 7e72e330d8..fa9bab9192 100644
--- a/resources/profiles/Prusa/filament/Generic PETG @Prusa CORE One INDX.json
+++ b/resources/profiles/Prusa/filament/Generic PETG @Prusa CORE One INDX.json
@@ -5,6 +5,9 @@
"from": "system",
"setting_id": "HbevqxqvYHuvRc3v",
"instantiation": "true",
+ "include": [
+ "fdm_filament_template_indx"
+ ],
"filament_cost": [
"27.82"
],
diff --git a/resources/profiles/Prusa/filament/Generic PETG @Prusa CORE One.json b/resources/profiles/Prusa/filament/Generic PETG @Prusa CORE One.json
index b605f77a89..4f3e7f400c 100644
--- a/resources/profiles/Prusa/filament/Generic PETG @Prusa CORE One.json
+++ b/resources/profiles/Prusa/filament/Generic PETG @Prusa CORE One.json
@@ -6,6 +6,9 @@
"from": "system",
"setting_id": "W71PW5bpf7CV16D4",
"instantiation": "true",
+ "include": [
+ "fdm_filament_template_nextruder_pet"
+ ],
"chamber_temperature": [
"35"
],
diff --git a/resources/profiles/Prusa/filament/Generic PETG @Prusa MK4 MMU3.json b/resources/profiles/Prusa/filament/Generic PETG @Prusa MK4 MMU3.json
new file mode 100644
index 0000000000..ec32594010
--- /dev/null
+++ b/resources/profiles/Prusa/filament/Generic PETG @Prusa MK4 MMU3.json
@@ -0,0 +1,19 @@
+{
+ "type": "filament",
+ "name": "Generic PETG @Prusa MK4 MMU3",
+ "inherits": "Generic PETG @Prusa MK4",
+ "from": "system",
+ "setting_id": "lBiZBFpSXZNIp5XG",
+ "instantiation": "true",
+ "include": [
+ "fdm_filament_template_nextruder_pet"
+ ],
+ "compatible_printers": [
+ "Prusa MK4 MMU3 0.4 nozzle",
+ "Prusa MK4 MMU3 0.6 nozzle",
+ "Prusa MK4 MMU3 HF0.4 nozzle"
+ ],
+ "filament_minimal_purge_on_wipe_tower": [
+ "35"
+ ]
+}
diff --git a/resources/profiles/Prusa/filament/Generic PLA @Prusa CORE One 0.6.json b/resources/profiles/Prusa/filament/Generic PLA @Prusa CORE One 0.6.json
index 009a827c7f..4718313292 100644
--- a/resources/profiles/Prusa/filament/Generic PLA @Prusa CORE One 0.6.json
+++ b/resources/profiles/Prusa/filament/Generic PLA @Prusa CORE One 0.6.json
@@ -8,12 +8,16 @@
"filament_id": "OFDSrzZ8",
"instantiation": "true",
"compatible_printers": [
- "Prusa CORE One 0.6 nozzle"
+ "Prusa CORE One 0.6 nozzle",
+ "Prusa CORE One MMU3 0.6 nozzle"
],
"nozzle_temperature": [
"210"
],
"slow_down_layer_time": [
"15"
+ ],
+ "filament_cooling_final_speed": [
+ "1.5"
]
}
diff --git a/resources/profiles/Prusa/filament/Generic PLA @Prusa CORE One HF 0.4.json b/resources/profiles/Prusa/filament/Generic PLA @Prusa CORE One HF 0.4.json
index e821c4d97d..cfaa133cb9 100644
--- a/resources/profiles/Prusa/filament/Generic PLA @Prusa CORE One HF 0.4.json
+++ b/resources/profiles/Prusa/filament/Generic PLA @Prusa CORE One HF 0.4.json
@@ -7,6 +7,9 @@
"setting_id": "vniEPaoyABoAfLFa",
"filament_id": "OFDSrzZ8",
"instantiation": "true",
+ "include": [
+ "fdm_filament_template_nextruder_pla_hf"
+ ],
"compatible_printers": [
"Prusa CORE One HF 0.4 nozzle",
"Prusa CORE One L HF 0.4 nozzle"
diff --git a/resources/profiles/Prusa/filament/Generic PLA @Prusa CORE One HF 0.5.json b/resources/profiles/Prusa/filament/Generic PLA @Prusa CORE One HF 0.5.json
index af02d26924..8ddcd915a8 100644
--- a/resources/profiles/Prusa/filament/Generic PLA @Prusa CORE One HF 0.5.json
+++ b/resources/profiles/Prusa/filament/Generic PLA @Prusa CORE One HF 0.5.json
@@ -7,6 +7,9 @@
"setting_id": "NPEs4gtBlKWupYZX",
"filament_id": "OFDSrzZ8",
"instantiation": "true",
+ "include": [
+ "fdm_filament_template_nextruder_pla_hf"
+ ],
"compatible_printers": [
"Prusa CORE One HF 0.5 nozzle"
],
diff --git a/resources/profiles/Prusa/filament/Generic PLA @Prusa CORE One HF 0.6.json b/resources/profiles/Prusa/filament/Generic PLA @Prusa CORE One HF 0.6.json
index dd830e04cd..912bd0d131 100644
--- a/resources/profiles/Prusa/filament/Generic PLA @Prusa CORE One HF 0.6.json
+++ b/resources/profiles/Prusa/filament/Generic PLA @Prusa CORE One HF 0.6.json
@@ -7,6 +7,9 @@
"setting_id": "SnfULjrWGCiA7ljq",
"filament_id": "OFDSrzZ8",
"instantiation": "true",
+ "include": [
+ "fdm_filament_template_nextruder_pla_hf"
+ ],
"compatible_printers": [
"Prusa CORE One HF 0.6 nozzle"
],
@@ -24,5 +27,8 @@
],
"slow_down_min_speed": [
"15"
+ ],
+ "filament_cooling_final_speed": [
+ "1.5"
]
}
diff --git a/resources/profiles/Prusa/filament/Generic PLA @Prusa CORE One INDX.json b/resources/profiles/Prusa/filament/Generic PLA @Prusa CORE One INDX.json
index 98583cf4b3..8c3c1e2a15 100644
--- a/resources/profiles/Prusa/filament/Generic PLA @Prusa CORE One INDX.json
+++ b/resources/profiles/Prusa/filament/Generic PLA @Prusa CORE One INDX.json
@@ -5,6 +5,9 @@
"from": "system",
"setting_id": "4qjcaIf1vk3K1x4Z",
"instantiation": "true",
+ "include": [
+ "fdm_filament_template_indx"
+ ],
"filament_cost": [
"25.4"
],
diff --git a/resources/profiles/Prusa/filament/Generic PLA @Prusa CORE One.json b/resources/profiles/Prusa/filament/Generic PLA @Prusa CORE One.json
index 247ebd2763..6f19c61dc6 100644
--- a/resources/profiles/Prusa/filament/Generic PLA @Prusa CORE One.json
+++ b/resources/profiles/Prusa/filament/Generic PLA @Prusa CORE One.json
@@ -6,6 +6,9 @@
"from": "system",
"setting_id": "r2i4c7DsYXYIMVCX",
"instantiation": "true",
+ "include": [
+ "fdm_filament_template_nextruder_pla"
+ ],
"chamber_temperature": [
"20"
],
@@ -17,8 +20,7 @@
"Prusa CORE One L 0.4 nozzle",
"Prusa CORE One MMU3 0.4 nozzle",
"Prusa CORE One MMU3 0.25 nozzle",
- "Prusa CORE One MMU3 0.3 nozzle",
- "Prusa CORE One MMU3 0.6 nozzle"
+ "Prusa CORE One MMU3 0.3 nozzle"
],
"fan_cooling_layer_time": [
"40"
diff --git a/resources/profiles/Prusa/filament/Generic PLA @Prusa MK4 MMU3.json b/resources/profiles/Prusa/filament/Generic PLA @Prusa MK4 MMU3.json
new file mode 100644
index 0000000000..a83fbb411d
--- /dev/null
+++ b/resources/profiles/Prusa/filament/Generic PLA @Prusa MK4 MMU3.json
@@ -0,0 +1,16 @@
+{
+ "type": "filament",
+ "name": "Generic PLA @Prusa MK4 MMU3",
+ "inherits": "Generic PLA @Prusa MK4",
+ "from": "system",
+ "setting_id": "kZvIh84niJ487FsY",
+ "instantiation": "true",
+ "include": [
+ "fdm_filament_template_nextruder_pla"
+ ],
+ "compatible_printers": [
+ "Prusa MK4 MMU3 0.4 nozzle",
+ "Prusa MK4 MMU3 0.6 nozzle",
+ "Prusa MK4 MMU3 HF0.4 nozzle"
+ ]
+}
diff --git a/resources/profiles/Prusa/filament/Generic PLA Silk @Prusa CORE One 0.6.json b/resources/profiles/Prusa/filament/Generic PLA Silk @Prusa CORE One 0.6.json
index 55f3698ba3..31000de70a 100644
--- a/resources/profiles/Prusa/filament/Generic PLA Silk @Prusa CORE One 0.6.json
+++ b/resources/profiles/Prusa/filament/Generic PLA Silk @Prusa CORE One 0.6.json
@@ -29,5 +29,8 @@
],
"slow_down_layer_time": [
"15"
+ ],
+ "filament_cooling_final_speed": [
+ "1.5"
]
}
diff --git a/resources/profiles/Prusa/filament/Generic PLA Silk @Prusa CORE One INDX.json b/resources/profiles/Prusa/filament/Generic PLA Silk @Prusa CORE One INDX.json
index 4c180da592..d4791320a9 100644
--- a/resources/profiles/Prusa/filament/Generic PLA Silk @Prusa CORE One INDX.json
+++ b/resources/profiles/Prusa/filament/Generic PLA Silk @Prusa CORE One INDX.json
@@ -5,6 +5,9 @@
"from": "system",
"setting_id": "eKla6su22rYaL9iz",
"instantiation": "true",
+ "include": [
+ "fdm_filament_template_indx"
+ ],
"filament_flow_ratio": [
"1",
"1",
diff --git a/resources/profiles/Prusa/filament/Generic PLA Silk @Prusa CORE One.json b/resources/profiles/Prusa/filament/Generic PLA Silk @Prusa CORE One.json
index 71aa3d915d..d22f3c6c6c 100644
--- a/resources/profiles/Prusa/filament/Generic PLA Silk @Prusa CORE One.json
+++ b/resources/profiles/Prusa/filament/Generic PLA Silk @Prusa CORE One.json
@@ -6,6 +6,9 @@
"from": "system",
"setting_id": "F9lNHyhZwZi7aD74",
"instantiation": "true",
+ "include": [
+ "fdm_filament_template_nextruder_pla"
+ ],
"chamber_temperature": [
"20"
],
diff --git a/resources/profiles/Prusa/filament/Prusament ASA @CORE One HF 0.4.json b/resources/profiles/Prusa/filament/Prusament ASA @CORE One HF 0.4.json
index e7977e0c94..19287185da 100644
--- a/resources/profiles/Prusa/filament/Prusament ASA @CORE One HF 0.4.json
+++ b/resources/profiles/Prusa/filament/Prusament ASA @CORE One HF 0.4.json
@@ -6,6 +6,9 @@
"setting_id": "tXktQXr79am1s5UL",
"filament_id": "OFmFwUWM",
"instantiation": "true",
+ "include": [
+ "fdm_filament_template_nextruder_pet_hf"
+ ],
"compatible_printers": [
"Prusa CORE One HF 0.4 nozzle"
],
diff --git a/resources/profiles/Prusa/filament/Prusament ASA @CORE One HF 0.5.json b/resources/profiles/Prusa/filament/Prusament ASA @CORE One HF 0.5.json
index 26e053cd79..64c8a59399 100644
--- a/resources/profiles/Prusa/filament/Prusament ASA @CORE One HF 0.5.json
+++ b/resources/profiles/Prusa/filament/Prusament ASA @CORE One HF 0.5.json
@@ -6,6 +6,9 @@
"setting_id": "YbyojtjXvEtI4Tt3",
"filament_id": "OFmFwUWM",
"instantiation": "true",
+ "include": [
+ "fdm_filament_template_nextruder_pet_hf"
+ ],
"compatible_printers": [
"Prusa CORE One HF 0.5 nozzle"
],
diff --git a/resources/profiles/Prusa/filament/Prusament ASA @CORE One HF 0.6.json b/resources/profiles/Prusa/filament/Prusament ASA @CORE One HF 0.6.json
index 5bde9d8869..9962f5cc76 100644
--- a/resources/profiles/Prusa/filament/Prusament ASA @CORE One HF 0.6.json
+++ b/resources/profiles/Prusa/filament/Prusament ASA @CORE One HF 0.6.json
@@ -6,6 +6,9 @@
"setting_id": "35H9TLr1p2Jy767g",
"filament_id": "OFmFwUWM",
"instantiation": "true",
+ "include": [
+ "fdm_filament_template_nextruder_pet_hf"
+ ],
"compatible_printers": [
"Prusa CORE One HF 0.6 nozzle"
],
diff --git a/resources/profiles/Prusa/filament/Prusament ASA @CORE One.json b/resources/profiles/Prusa/filament/Prusament ASA @CORE One.json
index fef0f31669..739b511db9 100644
--- a/resources/profiles/Prusa/filament/Prusament ASA @CORE One.json
+++ b/resources/profiles/Prusa/filament/Prusament ASA @CORE One.json
@@ -7,6 +7,9 @@
"setting_id": "UUTjJ4Y8St3c87jD",
"filament_id": "OFmFwUWM",
"instantiation": "true",
+ "include": [
+ "fdm_filament_template_nextruder_pet"
+ ],
"chamber_temperature": [
"40"
],
diff --git a/resources/profiles/Prusa/filament/Prusament ASA @MK4 MMU3.json b/resources/profiles/Prusa/filament/Prusament ASA @MK4 MMU3.json
new file mode 100644
index 0000000000..249fd6f025
--- /dev/null
+++ b/resources/profiles/Prusa/filament/Prusament ASA @MK4 MMU3.json
@@ -0,0 +1,19 @@
+{
+ "type": "filament",
+ "name": "Prusament ASA @MK4 MMU3",
+ "inherits": "Prusament ASA @MK4",
+ "from": "system",
+ "setting_id": "IRJ2dtBgjCMfmpOH",
+ "instantiation": "true",
+ "include": [
+ "fdm_filament_template_nextruder_pet"
+ ],
+ "compatible_printers": [
+ "Prusa MK4 MMU3 0.4 nozzle",
+ "Prusa MK4 MMU3 0.6 nozzle",
+ "Prusa MK4 MMU3 HF0.4 nozzle"
+ ],
+ "filament_minimal_purge_on_wipe_tower": [
+ "35"
+ ]
+}
diff --git a/resources/profiles/Prusa/filament/Prusament PA11 Carbon Fiber @CORE One.json b/resources/profiles/Prusa/filament/Prusament PA11 Carbon Fiber @CORE One.json
index 0913f1ea9a..d32a445e21 100644
--- a/resources/profiles/Prusa/filament/Prusament PA11 Carbon Fiber @CORE One.json
+++ b/resources/profiles/Prusa/filament/Prusament PA11 Carbon Fiber @CORE One.json
@@ -7,6 +7,9 @@
"setting_id": "sNGgucQjiFXwuFkq",
"filament_id": "OF4MZtnE",
"instantiation": "true",
+ "include": [
+ "fdm_filament_template_nextruder_pet"
+ ],
"chamber_temperature": [
"40"
],
diff --git a/resources/profiles/Prusa/filament/Prusament PC Blend @CORE One HF 0.4.json b/resources/profiles/Prusa/filament/Prusament PC Blend @CORE One HF 0.4.json
index 4e5b5643bd..bdcf80c13c 100644
--- a/resources/profiles/Prusa/filament/Prusament PC Blend @CORE One HF 0.4.json
+++ b/resources/profiles/Prusa/filament/Prusament PC Blend @CORE One HF 0.4.json
@@ -6,6 +6,9 @@
"setting_id": "pEsP537TBR5kozQB",
"filament_id": "OFvDbkFq",
"instantiation": "true",
+ "include": [
+ "fdm_filament_template_nextruder_pet_hf"
+ ],
"compatible_printers": [
"Prusa CORE One HF 0.4 nozzle"
],
diff --git a/resources/profiles/Prusa/filament/Prusament PC Blend @CORE One HF 0.5.json b/resources/profiles/Prusa/filament/Prusament PC Blend @CORE One HF 0.5.json
index ab9a127df8..0e82457803 100644
--- a/resources/profiles/Prusa/filament/Prusament PC Blend @CORE One HF 0.5.json
+++ b/resources/profiles/Prusa/filament/Prusament PC Blend @CORE One HF 0.5.json
@@ -6,6 +6,9 @@
"setting_id": "3IfLi1ZVCKKbEBUW",
"filament_id": "OFvDbkFq",
"instantiation": "true",
+ "include": [
+ "fdm_filament_template_nextruder_pet_hf"
+ ],
"compatible_printers": [
"Prusa CORE One HF 0.5 nozzle"
],
diff --git a/resources/profiles/Prusa/filament/Prusament PC Blend @CORE One HF 0.6.json b/resources/profiles/Prusa/filament/Prusament PC Blend @CORE One HF 0.6.json
index 642f421819..9d72d40e58 100644
--- a/resources/profiles/Prusa/filament/Prusament PC Blend @CORE One HF 0.6.json
+++ b/resources/profiles/Prusa/filament/Prusament PC Blend @CORE One HF 0.6.json
@@ -6,6 +6,9 @@
"setting_id": "Q8qWw2G7l2UvxGut",
"filament_id": "OFvDbkFq",
"instantiation": "true",
+ "include": [
+ "fdm_filament_template_nextruder_pet_hf"
+ ],
"compatible_printers": [
"Prusa CORE One HF 0.6 nozzle"
],
diff --git a/resources/profiles/Prusa/filament/Prusament PC Blend @CORE One.json b/resources/profiles/Prusa/filament/Prusament PC Blend @CORE One.json
index 1ef6ad5868..00b62e3e37 100644
--- a/resources/profiles/Prusa/filament/Prusament PC Blend @CORE One.json
+++ b/resources/profiles/Prusa/filament/Prusament PC Blend @CORE One.json
@@ -7,6 +7,9 @@
"setting_id": "x1SkWhSSkVVIPwCF",
"filament_id": "OFvDbkFq",
"instantiation": "true",
+ "include": [
+ "fdm_filament_template_nextruder_pet"
+ ],
"chamber_temperature": [
"40"
],
diff --git a/resources/profiles/Prusa/filament/Prusament PC Blend @MK4 MMU3.json b/resources/profiles/Prusa/filament/Prusament PC Blend @MK4 MMU3.json
new file mode 100644
index 0000000000..d728830afd
--- /dev/null
+++ b/resources/profiles/Prusa/filament/Prusament PC Blend @MK4 MMU3.json
@@ -0,0 +1,16 @@
+{
+ "type": "filament",
+ "name": "Prusament PC Blend @MK4 MMU3",
+ "inherits": "Prusament PC Blend @MK4",
+ "from": "system",
+ "setting_id": "gmNuCspHHxgP6zH8",
+ "instantiation": "true",
+ "include": [
+ "fdm_filament_template_nextruder_pet"
+ ],
+ "compatible_printers": [
+ "Prusa MK4 MMU3 0.4 nozzle",
+ "Prusa MK4 MMU3 0.6 nozzle",
+ "Prusa MK4 MMU3 HF0.4 nozzle"
+ ]
+}
diff --git a/resources/profiles/Prusa/filament/Prusament PC Blend Carbon Fiber @CORE One.json b/resources/profiles/Prusa/filament/Prusament PC Blend Carbon Fiber @CORE One.json
index d691595bf5..edaad073df 100644
--- a/resources/profiles/Prusa/filament/Prusament PC Blend Carbon Fiber @CORE One.json
+++ b/resources/profiles/Prusa/filament/Prusament PC Blend Carbon Fiber @CORE One.json
@@ -7,6 +7,9 @@
"setting_id": "B8Cm9l8i9osp5J2q",
"filament_id": "OFdO6vpt",
"instantiation": "true",
+ "include": [
+ "fdm_filament_template_nextruder_pet"
+ ],
"chamber_temperature": [
"40"
],
diff --git a/resources/profiles/Prusa/filament/Prusament PETG @CORE One HF 0.4.json b/resources/profiles/Prusa/filament/Prusament PETG @CORE One HF 0.4.json
index f34dcd29a7..5fc23cd3dc 100644
--- a/resources/profiles/Prusa/filament/Prusament PETG @CORE One HF 0.4.json
+++ b/resources/profiles/Prusa/filament/Prusament PETG @CORE One HF 0.4.json
@@ -6,6 +6,9 @@
"setting_id": "lwT1VxW5OkY36kUX",
"filament_id": "OFkR8E2c",
"instantiation": "true",
+ "include": [
+ "fdm_filament_template_nextruder_pet_hf"
+ ],
"compatible_printers": [
"Prusa CORE One HF 0.4 nozzle"
],
diff --git a/resources/profiles/Prusa/filament/Prusament PETG @CORE One HF 0.5.json b/resources/profiles/Prusa/filament/Prusament PETG @CORE One HF 0.5.json
index 71001294fa..527974207b 100644
--- a/resources/profiles/Prusa/filament/Prusament PETG @CORE One HF 0.5.json
+++ b/resources/profiles/Prusa/filament/Prusament PETG @CORE One HF 0.5.json
@@ -6,6 +6,9 @@
"setting_id": "XAgW7fElXxhaKaBT",
"filament_id": "OFkR8E2c",
"instantiation": "true",
+ "include": [
+ "fdm_filament_template_nextruder_pet_hf"
+ ],
"compatible_printers": [
"Prusa CORE One HF 0.5 nozzle"
],
diff --git a/resources/profiles/Prusa/filament/Prusament PETG @CORE One HF 0.6.json b/resources/profiles/Prusa/filament/Prusament PETG @CORE One HF 0.6.json
index cea5ef7809..271b04399f 100644
--- a/resources/profiles/Prusa/filament/Prusament PETG @CORE One HF 0.6.json
+++ b/resources/profiles/Prusa/filament/Prusament PETG @CORE One HF 0.6.json
@@ -6,6 +6,9 @@
"setting_id": "6aTGEZj2EmpTR1mu",
"filament_id": "OFkR8E2c",
"instantiation": "true",
+ "include": [
+ "fdm_filament_template_nextruder_pet_hf"
+ ],
"compatible_printers": [
"Prusa CORE One HF 0.6 nozzle"
],
diff --git a/resources/profiles/Prusa/filament/Prusament PETG @CORE One.json b/resources/profiles/Prusa/filament/Prusament PETG @CORE One.json
index 39090d8977..51aaba3419 100644
--- a/resources/profiles/Prusa/filament/Prusament PETG @CORE One.json
+++ b/resources/profiles/Prusa/filament/Prusament PETG @CORE One.json
@@ -7,6 +7,9 @@
"setting_id": "LSvba9ds3XTwDMCG",
"filament_id": "OFkR8E2c",
"instantiation": "true",
+ "include": [
+ "fdm_filament_template_nextruder_pet"
+ ],
"chamber_temperature": [
"35"
],
diff --git a/resources/profiles/Prusa/filament/Prusament PETG @MK4 MMU3.json b/resources/profiles/Prusa/filament/Prusament PETG @MK4 MMU3.json
new file mode 100644
index 0000000000..9e3caf2d9f
--- /dev/null
+++ b/resources/profiles/Prusa/filament/Prusament PETG @MK4 MMU3.json
@@ -0,0 +1,19 @@
+{
+ "type": "filament",
+ "name": "Prusament PETG @MK4 MMU3",
+ "inherits": "Prusament PETG @MK4",
+ "from": "system",
+ "setting_id": "7DQ6YEKyQKYDTKtE",
+ "instantiation": "true",
+ "include": [
+ "fdm_filament_template_nextruder_pet"
+ ],
+ "compatible_printers": [
+ "Prusa MK4 MMU3 0.4 nozzle",
+ "Prusa MK4 MMU3 0.6 nozzle",
+ "Prusa MK4 MMU3 HF0.4 nozzle"
+ ],
+ "filament_minimal_purge_on_wipe_tower": [
+ "35"
+ ]
+}
diff --git a/resources/profiles/Prusa/filament/Prusament PETG @Prusa CORE One INDX.json b/resources/profiles/Prusa/filament/Prusament PETG @Prusa CORE One INDX.json
index 0fdb644150..14a4494246 100644
--- a/resources/profiles/Prusa/filament/Prusament PETG @Prusa CORE One INDX.json
+++ b/resources/profiles/Prusa/filament/Prusament PETG @Prusa CORE One INDX.json
@@ -6,6 +6,9 @@
"setting_id": "2viuOmBvXuQm2FwV",
"filament_id": "OFkR8E2c",
"instantiation": "true",
+ "include": [
+ "fdm_filament_template_indx"
+ ],
"filament_flow_ratio": [
"1"
],
diff --git a/resources/profiles/Prusa/filament/Prusament PLA @CORE One HF 0.4.json b/resources/profiles/Prusa/filament/Prusament PLA @CORE One HF 0.4.json
index 8c1d806d98..a4e85823dd 100644
--- a/resources/profiles/Prusa/filament/Prusament PLA @CORE One HF 0.4.json
+++ b/resources/profiles/Prusa/filament/Prusament PLA @CORE One HF 0.4.json
@@ -6,6 +6,9 @@
"setting_id": "mf4Muog4IcsgkYoA",
"filament_id": "OFnO4wnf",
"instantiation": "true",
+ "include": [
+ "fdm_filament_template_nextruder_pla_hf"
+ ],
"compatible_printers": [
"Prusa CORE One HF 0.4 nozzle",
"Prusa CORE One L HF 0.4 nozzle"
diff --git a/resources/profiles/Prusa/filament/Prusament PLA @CORE One.json b/resources/profiles/Prusa/filament/Prusament PLA @CORE One.json
index e90e22ad51..a28ea07ed4 100644
--- a/resources/profiles/Prusa/filament/Prusament PLA @CORE One.json
+++ b/resources/profiles/Prusa/filament/Prusament PLA @CORE One.json
@@ -7,6 +7,9 @@
"setting_id": "PuujUMjNKLZvZNAH",
"filament_id": "OFnO4wnf",
"instantiation": "true",
+ "include": [
+ "fdm_filament_template_nextruder_pla"
+ ],
"chamber_temperature": [
"20"
],
@@ -104,5 +107,11 @@
],
"slow_down_min_speed": [
"20"
+ ],
+ "filament_unloading_speed": [
+ "100"
+ ],
+ "filament_unloading_speed_start": [
+ "100"
]
}
diff --git a/resources/profiles/Prusa/filament/Prusament PLA @MK4 MMU3.json b/resources/profiles/Prusa/filament/Prusament PLA @MK4 MMU3.json
new file mode 100644
index 0000000000..d52b958727
--- /dev/null
+++ b/resources/profiles/Prusa/filament/Prusament PLA @MK4 MMU3.json
@@ -0,0 +1,22 @@
+{
+ "type": "filament",
+ "name": "Prusament PLA @MK4 MMU3",
+ "inherits": "Prusament PLA @MK4",
+ "from": "system",
+ "setting_id": "MsDcN4npnmLRXOjm",
+ "instantiation": "true",
+ "include": [
+ "fdm_filament_template_nextruder_pla"
+ ],
+ "compatible_printers": [
+ "Prusa MK4 MMU3 0.4 nozzle",
+ "Prusa MK4 MMU3 0.6 nozzle",
+ "Prusa MK4 MMU3 HF0.4 nozzle"
+ ],
+ "filament_unloading_speed": [
+ "100"
+ ],
+ "filament_unloading_speed_start": [
+ "100"
+ ]
+}
diff --git a/resources/profiles/Prusa/filament/Prusament PLA @Prusa CORE One INDX.json b/resources/profiles/Prusa/filament/Prusament PLA @Prusa CORE One INDX.json
index f5f64a98b3..82fdd349da 100644
--- a/resources/profiles/Prusa/filament/Prusament PLA @Prusa CORE One INDX.json
+++ b/resources/profiles/Prusa/filament/Prusament PLA @Prusa CORE One INDX.json
@@ -6,6 +6,9 @@
"setting_id": "B2UB9QUbbjpn12Ka",
"filament_id": "OFnO4wnf",
"instantiation": "true",
+ "include": [
+ "fdm_filament_template_indx"
+ ],
"filament_flow_ratio": [
"1"
],
diff --git a/resources/profiles/Prusa/filament/Prusament PVB @CORE One.json b/resources/profiles/Prusa/filament/Prusament PVB @CORE One.json
index 56987a8093..9f6786bd38 100644
--- a/resources/profiles/Prusa/filament/Prusament PVB @CORE One.json
+++ b/resources/profiles/Prusa/filament/Prusament PVB @CORE One.json
@@ -7,6 +7,9 @@
"setting_id": "085dGkrnPhatF07x",
"filament_id": "OFh7mvPO",
"instantiation": "true",
+ "include": [
+ "fdm_filament_template_nextruder_pla"
+ ],
"chamber_temperature": [
"20"
],
@@ -105,5 +108,11 @@
],
"slow_down_min_speed": [
"20"
+ ],
+ "filament_unloading_speed": [
+ "60"
+ ],
+ "filament_unloading_speed_start": [
+ "60"
]
}
diff --git a/resources/profiles/Prusa/filament/Prusament rPLA @CORE One 0.6.json b/resources/profiles/Prusa/filament/Prusament rPLA @CORE One 0.6.json
index adbf76e56d..0af7c24c5e 100644
--- a/resources/profiles/Prusa/filament/Prusament rPLA @CORE One 0.6.json
+++ b/resources/profiles/Prusa/filament/Prusament rPLA @CORE One 0.6.json
@@ -8,12 +8,16 @@
"instantiation": "true",
"compatible_printers": [
"Prusa CORE One 0.6 nozzle",
- "Prusa CORE One HF 0.6 nozzle"
+ "Prusa CORE One HF 0.6 nozzle",
+ "Prusa CORE One MMU3 0.6 nozzle"
],
"filament_max_volumetric_speed": [
"14"
],
"slow_down_layer_time": [
"15"
+ ],
+ "filament_cooling_final_speed": [
+ "1.5"
]
}
diff --git a/resources/profiles/Prusa/filament/Prusament rPLA @CORE One.json b/resources/profiles/Prusa/filament/Prusament rPLA @CORE One.json
index 7f41ae32e4..73633d6044 100644
--- a/resources/profiles/Prusa/filament/Prusament rPLA @CORE One.json
+++ b/resources/profiles/Prusa/filament/Prusament rPLA @CORE One.json
@@ -7,6 +7,9 @@
"setting_id": "Bw6H4vFnq7YExyLd",
"filament_id": "OFWRITFw",
"instantiation": "true",
+ "include": [
+ "fdm_filament_template_nextruder_pla"
+ ],
"chamber_temperature": [
"20"
],
@@ -22,8 +25,7 @@
"Prusa CORE One HF 0.5 nozzle",
"Prusa CORE One MMU3 0.4 nozzle",
"Prusa CORE One MMU3 0.25 nozzle",
- "Prusa CORE One MMU3 0.3 nozzle",
- "Prusa CORE One MMU3 0.6 nozzle"
+ "Prusa CORE One MMU3 0.3 nozzle"
],
"fan_cooling_layer_time": [
"40"
diff --git a/resources/profiles/Prusa/filament/Prusament rPLA @MK4 MMU3.json b/resources/profiles/Prusa/filament/Prusament rPLA @MK4 MMU3.json
new file mode 100644
index 0000000000..e8b182bbf0
--- /dev/null
+++ b/resources/profiles/Prusa/filament/Prusament rPLA @MK4 MMU3.json
@@ -0,0 +1,16 @@
+{
+ "type": "filament",
+ "name": "Prusament rPLA @MK4 MMU3",
+ "inherits": "Prusament rPLA @MK4",
+ "from": "system",
+ "setting_id": "wKhvyGpgjmW6glLj",
+ "instantiation": "true",
+ "include": [
+ "fdm_filament_template_nextruder_pla"
+ ],
+ "compatible_printers": [
+ "Prusa MK4 MMU3 0.4 nozzle",
+ "Prusa MK4 MMU3 0.6 nozzle",
+ "Prusa MK4 MMU3 HF0.4 nozzle"
+ ]
+}
diff --git a/resources/profiles/Prusa/filament/fdm_filament_template_indx.json b/resources/profiles/Prusa/filament/fdm_filament_template_indx.json
new file mode 100644
index 0000000000..4aea931123
--- /dev/null
+++ b/resources/profiles/Prusa/filament/fdm_filament_template_indx.json
@@ -0,0 +1,21 @@
+{
+ "type": "filament",
+ "name": "fdm_filament_template_indx",
+ "from": "system",
+ "instantiation": "false",
+ "filament_multitool_ramming": [
+ "1"
+ ],
+ "filament_multitool_ramming_flow": [
+ "30"
+ ],
+ "filament_multitool_ramming_volume": [
+ "18"
+ ],
+ "filament_minimal_purge_on_wipe_tower": [
+ "10"
+ ],
+ "filament_start_gcode": [
+ "M572 S{pressure_advance[filament_extruder_id]} ; Pressure advance\nM573 R"
+ ]
+}
diff --git a/resources/profiles/Prusa/filament/fdm_filament_template_nextruder_pet.json b/resources/profiles/Prusa/filament/fdm_filament_template_nextruder_pet.json
new file mode 100644
index 0000000000..ba12bb2e62
--- /dev/null
+++ b/resources/profiles/Prusa/filament/fdm_filament_template_nextruder_pet.json
@@ -0,0 +1,36 @@
+{
+ "type": "filament",
+ "name": "fdm_filament_template_nextruder_pet",
+ "from": "system",
+ "instantiation": "false",
+ "filament_ramming_parameters": [
+ "250 100 42.4194 42.4194 42.4194 42.4194 42.4194| 0.05 42.4387 0.45 42.4387 0.95 42.4387 1.45 42.4387 1.95 42.4387 2.45 42.4387 2.95 42.4387 3.45 42.4387 3.95 42.4387 4.45 42.4387 4.95 42.4387"
+ ],
+ "filament_loading_speed": [
+ "10"
+ ],
+ "filament_loading_speed_start": [
+ "50"
+ ],
+ "filament_unloading_speed": [
+ "60"
+ ],
+ "filament_unloading_speed_start": [
+ "60"
+ ],
+ "filament_cooling_moves": [
+ "3"
+ ],
+ "filament_cooling_initial_speed": [
+ "5"
+ ],
+ "filament_cooling_final_speed": [
+ "2.5"
+ ],
+ "filament_stamping_distance": [
+ "45"
+ ],
+ "filament_stamping_loading_speed": [
+ "26.5"
+ ]
+}
diff --git a/resources/profiles/Prusa/filament/fdm_filament_template_nextruder_pet_hf.json b/resources/profiles/Prusa/filament/fdm_filament_template_nextruder_pet_hf.json
new file mode 100644
index 0000000000..ba16fadbad
--- /dev/null
+++ b/resources/profiles/Prusa/filament/fdm_filament_template_nextruder_pet_hf.json
@@ -0,0 +1,9 @@
+{
+ "type": "filament",
+ "name": "fdm_filament_template_nextruder_pet_hf",
+ "from": "system",
+ "instantiation": "false",
+ "filament_ramming_parameters": [
+ "250 100 42.4194 42.4194| 0.05 42.4387 0.45 42.4387 0.95 42.4387 1.45 42.4387 1.95 42.4387 2.45 42.4387 2.95 42.4387 3.45 42.4387 3.95 42.4387 4.45 42.4387 4.95 42.4387"
+ ]
+}
diff --git a/resources/profiles/Prusa/filament/fdm_filament_template_nextruder_pla.json b/resources/profiles/Prusa/filament/fdm_filament_template_nextruder_pla.json
new file mode 100644
index 0000000000..d7b2a4b381
--- /dev/null
+++ b/resources/profiles/Prusa/filament/fdm_filament_template_nextruder_pla.json
@@ -0,0 +1,36 @@
+{
+ "type": "filament",
+ "name": "fdm_filament_template_nextruder_pla",
+ "from": "system",
+ "instantiation": "false",
+ "filament_ramming_parameters": [
+ "250 100 40.1613 40.3548 40.4516 40.3548 40.2581| 0.05 40.1483 0.45 40.3419 0.95 40.3419 1.45 40.3419 1.95 40.3419 2.45 40.3419 2.95 40.3419 3.45 40.3419 3.95 40.3419 4.45 40.3419 4.95 40.3419"
+ ],
+ "filament_loading_speed": [
+ "10"
+ ],
+ "filament_loading_speed_start": [
+ "50"
+ ],
+ "filament_unloading_speed": [
+ "40"
+ ],
+ "filament_unloading_speed_start": [
+ "30"
+ ],
+ "filament_cooling_moves": [
+ "2"
+ ],
+ "filament_cooling_initial_speed": [
+ "10"
+ ],
+ "filament_cooling_final_speed": [
+ "3.5"
+ ],
+ "filament_stamping_distance": [
+ "45"
+ ],
+ "filament_stamping_loading_speed": [
+ "29"
+ ]
+}
diff --git a/resources/profiles/Prusa/filament/fdm_filament_template_nextruder_pla_hf.json b/resources/profiles/Prusa/filament/fdm_filament_template_nextruder_pla_hf.json
new file mode 100644
index 0000000000..675668725c
--- /dev/null
+++ b/resources/profiles/Prusa/filament/fdm_filament_template_nextruder_pla_hf.json
@@ -0,0 +1,9 @@
+{
+ "type": "filament",
+ "name": "fdm_filament_template_nextruder_pla_hf",
+ "from": "system",
+ "instantiation": "false",
+ "filament_ramming_parameters": [
+ "250 100 39.871 39.871| 0.05 39.9547 0.45 39.9547 0.95 39.9547 1.45 39.9547 1.95 39.9547 2.45 39.9547 2.95 39.9547 3.45 39.9547 3.95 39.9547 4.45 39.9547 4.95 39.9547"
+ ]
+}
diff --git a/resources/profiles/Prusa/machine/Prusa CORE One MMU3.json b/resources/profiles/Prusa/machine/Prusa CORE One MMU3.json
index a9af70344e..fdbe0969df 100644
--- a/resources/profiles/Prusa/machine/Prusa CORE One MMU3.json
+++ b/resources/profiles/Prusa/machine/Prusa CORE One MMU3.json
@@ -3,7 +3,7 @@
"name": "Prusa CORE One MMU3",
"bed_model": "coreone_bed.stl",
"bed_texture": "coreone.svg",
- "default_materials": "Generic ABS @Prusa CORE One;Generic ASA @Prusa CORE One;Generic PETG @Prusa CORE One;Generic PLA @Prusa CORE One;Generic PLA Silk @Prusa CORE One;Generic TPU @Prusa CORE One",
+ "default_materials": "Generic ABS @Prusa CORE One;Generic ASA @Prusa CORE One;Generic PETG @Prusa CORE One;Generic PLA @Prusa CORE One;Generic PLA Silk @Prusa CORE One;Generic TPU @Prusa CORE One;Generic PLA @Prusa CORE One 0.6",
"family": "Prusa",
"hotend_model": "",
"machine_tech": "FFF",
diff --git a/resources/profiles/Prusa/machine/Prusa MK4 MMU3 0.4 nozzle.json b/resources/profiles/Prusa/machine/Prusa MK4 MMU3 0.4 nozzle.json
index a330559184..f89da08a0d 100644
--- a/resources/profiles/Prusa/machine/Prusa MK4 MMU3 0.4 nozzle.json
+++ b/resources/profiles/Prusa/machine/Prusa MK4 MMU3 0.4 nozzle.json
@@ -16,7 +16,7 @@
],
"printer_model": "Prusa MK4 MMU3",
"default_filament_profile": [
- "Generic PLA @System"
+ "Generic PLA @Prusa MK4 MMU3"
],
"default_print_profile": "0.20mm SPEED @MK4 MMU3 0.4",
"printer_notes": "Don't remove the following keywords! These keywords are used in the \"compatible printer\" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_MODEL_MK4IS\nPRINTER_MODEL_MK4MMU3\nPG"
diff --git a/resources/profiles/Prusa/machine/Prusa MK4 MMU3 0.6 nozzle.json b/resources/profiles/Prusa/machine/Prusa MK4 MMU3 0.6 nozzle.json
index dcbb537214..8b4fed8700 100644
--- a/resources/profiles/Prusa/machine/Prusa MK4 MMU3 0.6 nozzle.json
+++ b/resources/profiles/Prusa/machine/Prusa MK4 MMU3 0.6 nozzle.json
@@ -17,7 +17,7 @@
"printer_model": "Prusa MK4 MMU3",
"printer_variant": "0.6",
"default_filament_profile": [
- "Generic PLA @System"
+ "Generic PLA @Prusa MK4 MMU3"
],
"default_print_profile": "0.25mm SPEED @MK4 MMU3 0.6",
"printer_notes": "Don't remove the following keywords! These keywords are used in the \"compatible printer\" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_MODEL_MK4IS\nPRINTER_MODEL_MK4MMU3\nPG"
diff --git a/resources/profiles/Prusa/machine/Prusa MK4 MMU3 HF.json b/resources/profiles/Prusa/machine/Prusa MK4 MMU3 HF.json
index 11eaf998ec..1b33bcd514 100644
--- a/resources/profiles/Prusa/machine/Prusa MK4 MMU3 HF.json
+++ b/resources/profiles/Prusa/machine/Prusa MK4 MMU3 HF.json
@@ -8,5 +8,5 @@
"bed_model": "mk4_bed.stl",
"bed_texture": "mk4is.svg",
"hotend_model": "",
- "default_materials": "Generic PLA @System;Generic PETG @System;Generic ABS @System"
+ "default_materials": "Generic PLA @Prusa MK4 MMU3;Generic PETG @Prusa MK4 MMU3;Generic ABS @Prusa MK4 MMU3"
}
diff --git a/resources/profiles/Prusa/machine/Prusa MK4 MMU3 HF0.4 nozzle.json b/resources/profiles/Prusa/machine/Prusa MK4 MMU3 HF0.4 nozzle.json
index 9010b9d75d..95a8628e96 100644
--- a/resources/profiles/Prusa/machine/Prusa MK4 MMU3 HF0.4 nozzle.json
+++ b/resources/profiles/Prusa/machine/Prusa MK4 MMU3 HF0.4 nozzle.json
@@ -16,7 +16,7 @@
],
"printer_model": "Prusa MK4 MMU3 HF",
"default_filament_profile": [
- "Generic PLA @System"
+ "Generic PLA @Prusa MK4 MMU3"
],
"default_print_profile": "0.20mm SPEED @MK4 MMU3 HF0.4",
"printer_notes": "Don't remove the following keywords! These keywords are used in the \"compatible printer\" condition of the print and filament profiles to link the particular print and filament profiles to this printer profile.\nPRINTER_MODEL_MK4IS\nPRINTER_MODEL_MK4MMU3\nPG\nHF_NOZZLE"
diff --git a/resources/profiles/Prusa/machine/Prusa MK4 MMU3.json b/resources/profiles/Prusa/machine/Prusa MK4 MMU3.json
index 76a181b22a..cc071c0839 100644
--- a/resources/profiles/Prusa/machine/Prusa MK4 MMU3.json
+++ b/resources/profiles/Prusa/machine/Prusa MK4 MMU3.json
@@ -8,5 +8,5 @@
"bed_model": "mk4_bed.stl",
"bed_texture": "mk4is.svg",
"hotend_model": "",
- "default_materials": "Generic PLA @System;Generic PETG @System;Generic ABS @System"
+ "default_materials": "Generic PLA @Prusa MK4 MMU3;Generic PETG @Prusa MK4 MMU3;Generic ABS @Prusa MK4 MMU3"
}
diff --git a/scripts/CAD/check-gui-click-edit.py b/scripts/CAD/check-gui-click-edit.py
index 159bdb19f1..5eb9b3c4f2 100755
--- a/scripts/CAD/check-gui-click-edit.py
+++ b/scripts/CAD/check-gui-click-edit.py
@@ -147,6 +147,7 @@ def seed_datadir(datadir):
# ~90% case expects. A ladder whose result depends on the developer's own preferences is not
# a gate.
app["auto_close_sketch_loops"] = True
+ app["design_show_bed"] = True
# SILENCE THE NETWORK PLUGIN PROMPT. Without this, GUI_App::post_init() re-raises "Bambu
# Network Plug-in Required" from an IDLE event — after any modal sweep this driver does at
# startup — and ShowModal() then runs a nested event loop. The app is alive, its window is
@@ -480,10 +481,14 @@ def type_into_open_field(value, mark):
def enter_sketch(timeout=180):
"""Open a real sketch on a real plane, and PROVE it with the socket before drawing anything.
- THE SEQUENCE MATTERS AND IT IS NOT OBVIOUS. Shift+S enters sketch MODE and pops the plane
- offer; the offer must be dismissed; and the plane itself is chosen by clicking it in the
- viewport BEFORE Shift+S. check-gui-sketching.py has always done all four steps. This ladder
- did two of them — Design tab, then Shift+S — and went straight to the tool letters.
+ THE SEQUENCE MATTERS AND IT IS NOT OBVIOUS. The reference planes are hidden until Shift+S,
+ with nothing picked, brings them up to choose from, so the plane is clicked AFTER it. That
+ click opens the session on the plane and pops the tool offer, and a click outside a popup
+ menu only closes it, so the spot is clicked twice, apart: the second click closes the offer,
+ and inside the session it is empty canvas. No Escape — with no offer up, Escape in an empty
+ sketch leaves it. Then a tool letter arms the tool. check-gui-sketching.py does the same
+ steps. This ladder once did two of them — Design tab, then Shift+S — and went straight to
+ the tool letters.
That intermediate state is the trap. `is_sketching` reads 1, every tool key is accepted and
traced, and not one click draws anything, because there is no plane under them. The ladder
@@ -499,10 +504,10 @@ def enter_sketch(timeout=180):
click(132, 53) # Design tab
time.sleep(2.0)
dismiss_modals()
- click(*PLANE_PX) # pick the plane IN THE VIEWPORT — before Shift+S
- key("shift+s", 1.0)
- key("Escape", 0.5) # entering sketch mode pops the offer; dismiss it
- key("p", 0.6) # any sketch tool starts the session on that plane
+ key("shift+s", 1.0) # nothing picked: the reference planes come up
+ click(*PLANE_PX, pause=0.8) # picks the plane IN THE VIEWPORT: session open, offer up...
+ click(*PLANE_PX) # ...and this closes the offer
+ key("p", 0.6) # arms Point in the session
if try_call("sketch_describe") is not None:
# NO Escape here. Every rung already opens with one to drop whatever tool the last
# one left armed, and Escape in the Design tab walks a LIFO: first press drops the
@@ -511,14 +516,15 @@ def enter_sketch(timeout=180):
# reported all eleven checks failed with "nothing opened" — the tools were arming
# into an empty Feature-mode document.
return
- die("no sketch opened after plane click + Shift+S within "
+ die("no sketch opened after Shift+S + plane click within "
f"{timeout}s — sketch_describe never answered on {A.sock} (trace {TRACE})")
# tool key, the clicks that draw it, and one distinct value per queued field. The values are
# deliberately nothing like the as-drawn size, so a committed prefill cannot coincide with them.
-# Where the plane label sits in the viewport before a sketch is open. Same constant the gesture
-# ladder uses; it is a label on the 3D view, not a widget, so it moves only if the camera does.
+# Where the plane label sits in the viewport once Shift+S has brought the planes up. Same
+# constant the gesture ladder uses; it is a label on the 3D view, not a widget, so it moves only
+# if the camera does.
PLANE_PX = (913, 359)
# Every coordinate below stays inside 1000..1400 x 500..760 — the box check-gui-sketching.py's
diff --git a/scripts/CAD/check-gui-sketching.py b/scripts/CAD/check-gui-sketching.py
index d4ebd1f1ca..df83803819 100644
--- a/scripts/CAD/check-gui-sketching.py
+++ b/scripts/CAD/check-gui-sketching.py
@@ -229,7 +229,20 @@ CHROME_DY = int(os.environ.get("ORCA_CAD_CHROME_DY", "26"))
DESIGN_TAB = (128, 29 + CHROME_DY)
-# Feature-tree rows, measured on the rig at 1920x1080: first row centre, then 23 px apart.
+# The ribbon's document group lost its Bed checkbox and label when the bed toggle moved to the
+# Feature tree, and every group to its right moved left by their width: wxLEFT 6 + the 18 px
+# CheckBox + wxLEFT 4 + the "Bed" label (~24 px) + wxRIGHT 4, about 56 px. The x of
+# CONSTRUCTION_CHECKBOX, CON_BTN and CON_BTN_SKETCH below is the one measured before that, plus
+# this. DERIVED, NOT MEASURED: re-measure on the rig and fold it in. CONFIRM_BTN sits after the
+# ribbon's stretch spacer and did not move.
+RIBBON_DX = -56
+
+# Feature-tree rows, measured on the rig at 1920x1080: the list's first row is centred at y=215,
+# then 23 px apart. The first two rows are the Origin and the Bed, unframed on the card above the
+# features' own framed list, so the first FEATURE row sits two rows and TREE_GAP further down;
+# reset_document and the reopening double-click both need a feature. TREE_GAP is the list's
+# 12 px top margin plus its 1 px frame, derived rather than measured -- re-measure on the rig, like
+# RIBBON_DX.
# x=300, not the label: a second click ON the label opens the inline rename, and Delete then
# edits the text instead of removing the feature.
#
@@ -237,7 +250,8 @@ DESIGN_TAB = (128, 29 + CHROME_DY)
# unshifted click lands 26 px BELOW the first row -- just past its 23 px height -- so the row is
# never selected, Delete does nothing, and reset_document spends 40 rounds on it before dying
# with "could not empty the feature tree". That names the feature tree, which is not the fault.
-TREE_ROW0 = (300, 215 + CHROME_DY)
+TREE_GAP = 12 + 1
+TREE_ROW0 = (300, 215 + 2 * 23 + TREE_GAP + CHROME_DY)
def go_design():
@@ -266,18 +280,21 @@ def reset_document():
def enter_sketch(tool_key, plane_px=(913, 359)):
"""Enter a sketch the way the design law says: pick the plane in the viewport, then the tool.
- Shift+S enters sketch MODE and pops the offer; Escape dismisses it; the tool letter then
- starts the session on the plane the click selected. All four steps are real input — nothing
- here goes through the socket.
+ leave_sketch()'s Escapes let go of every pick, so Shift+S finds nothing to sketch on and only
+ brings the reference planes up; the click on one opens the session on it and pops the tool
+ offer. A click outside a popup menu only closes it, so the same spot is clicked again, apart:
+ inside the session it is empty canvas, so it selects nothing if the offer is slow to appear.
+ No Escape: with no offer up, Escape in an empty sketch leaves it. The tool letter then arms
+ the tool. All of it is real input — nothing here goes through the socket.
"""
leave_sketch()
- click(*plane_px)
key("shift+s", 0.8)
- key("Escape", 0.4) # entering sketch mode pops the offer; dismiss it
+ click(*plane_px, pause=0.8) # picks the plane: the session opens and the offer pops...
+ click(*plane_px) # ...and this closes the offer
key("p", 0.6)
if try_call("sketch_describe") is None:
shot("/shots/gl-enter-failed.png")
- die("no sketch opened after plane click + Shift+S (see /shots/gl-enter-failed.png)")
+ die("no sketch opened after Shift+S + plane click (see /shots/gl-enter-failed.png)")
calibrate_here() # THIS sketch's own camera map, on THIS sketch's own plane
key(tool_key, 0.6)
@@ -737,7 +754,7 @@ def corner_pair(ents):
die("no adjacent pair in what should be a rectangle")
-CONSTRUCTION_CHECKBOX = (419, 75 + CHROME_DY)
+CONSTRUCTION_CHECKBOX = (419 + RIBBON_DX, 75 + CHROME_DY)
def draw_line(x0, y0, x1, y1, length, angle):
@@ -866,7 +883,7 @@ def rung_extend():
# because the ladder only ever clicks "perpendicular" (3) and "equal" (5), both of which sit
# before the first insertion. The next rung to use "tangent" would have clicked "collinear".
CON_BTN_Y = 76 + CHROME_DY
-CON_BTN = {n: (449 + 42 * i, CON_BTN_Y) for i, n in enumerate(
+CON_BTN = {n: (449 + RIBBON_DX + 42 * i, CON_BTN_Y) for i, n in enumerate(
["horizontal", "vertical", "parallel", "perpendicular", "coincident", "equal",
"equal_radius", "collinear", "concentric", "tangent", "midpoint", "symmetric",
"sym_v", "sym_h", "angle", "radius", "diameter", "fix", "dist_x", "dist_y"])}
@@ -880,7 +897,7 @@ CON_BTN = {n: (449 + 42 * i, CON_BTN_Y) for i, n in enumerate(
# which already carries CHROME_DY), but the X start depends on how wide the sketch toolbar to the
# left of this group renders, and this fork keeps mainline's top row. Re-measure before trusting
# D11 here: screenshot in sketch mode and detect the icon columns, do not derive it by offset.
-CON_BTN_SKETCH = {n: (677 + 42 * i, CON_BTN_Y) for i, n in enumerate(
+CON_BTN_SKETCH = {n: (677 + RIBBON_DX + 42 * i, CON_BTN_Y) for i, n in enumerate(
["horizontal", "vertical", "parallel", "perpendicular", "coincident", "equal",
"equal_radius", "collinear", "concentric", "tangent", "midpoint", "symmetric",
"sym_v", "sym_h", "angle", "radius", "diameter", "fix", "dist_x", "dist_y"])}
diff --git a/scripts/CAD/tool_atlas.json b/scripts/CAD/tool_atlas.json
index 1e7f1bf968..af0653feff 100644
--- a/scripts/CAD/tool_atlas.json
+++ b/scripts/CAD/tool_atlas.json
@@ -229,11 +229,11 @@
"none"
],
"needs": {},
- "refusal": "Click a face or a reference plane in the viewport, then a sketch tool",
+ "refusal": "Select a flat face or a reference plane to sketch on",
"gui": true,
"action": "key:S+S",
"icon": "design_sketch",
- "hint": "Click a face or a reference plane, then pick a drawing tool"
+ "hint": "Sketch on the selected flat face or plane, or click one next"
},
{
"id": "extrude",
@@ -1113,6 +1113,32 @@
"icon": "color_palette",
"hint": "Set the selected body's display color"
},
+ {
+ "id": "zoom_to",
+ "name": "Zoom to selection",
+ "slot": "top",
+ "key": null,
+ "feature": null,
+ "mcp": null,
+ "mode": "model",
+ "accepts": [
+ "body_solid",
+ "body_sheet",
+ "face_planar",
+ "face_cyl",
+ "face_other",
+ "edge_str",
+ "edge_circ",
+ "vertex",
+ "sk_loop"
+ ],
+ "needs": {},
+ "refusal": null,
+ "gui": true,
+ "action": "btn:zoom_to",
+ "icon": "design_zoom",
+ "hint": "Frame the selection in the view, keeping the view direction"
+ },
{
"id": "delete",
"name": "Delete",
diff --git a/src/libslic3r/AppConfig.cpp b/src/libslic3r/AppConfig.cpp
index ebaa17a4b7..984524b0ba 100644
--- a/src/libslic3r/AppConfig.cpp
+++ b/src/libslic3r/AppConfig.cpp
@@ -370,6 +370,10 @@ void AppConfig::set_defaults()
// restores the conventional CAD representation for users who expect it (x0kd).
if (get("design_connector_face_glyph").empty())
set_bool("design_connector_face_glyph", true);
+
+ // Design tab: draw the printer bed and its plate grid (the Feature tree's Bed row).
+ if (get("design_show_bed").empty())
+ set_bool("design_show_bed", true);
#endif
//#ifdef SUPPORT_SHOW_HINTS
diff --git a/src/libslic3r/PerimeterGenerator.cpp b/src/libslic3r/PerimeterGenerator.cpp
index e2b5090213..41db0ed9db 100644
--- a/src/libslic3r/PerimeterGenerator.cpp
+++ b/src/libslic3r/PerimeterGenerator.cpp
@@ -61,6 +61,8 @@ public:
bool is_smaller_width_perimeter;
// Depth in the hierarchy. External perimeter has depth = 0. An external perimeter could be both a contour and a hole.
unsigned short depth;
+ // ORCA: an external hole perimeter touching the next outer wall all around, with nothing between them.
+ bool is_thin_wall_hole = false;
// Children contour, may be both CCW and CW oriented (outer contours or holes).
std::vector children;
@@ -118,7 +120,7 @@ static bool detect_steep_overhang(const PrintRegionConfig *config,
}
static ExtrusionEntityCollection traverse_loops(const PerimeterGenerator &perimeter_generator, const PerimeterGeneratorLoops &loops, ThickPolylines &thin_walls,
- bool &steep_overhang_contour, bool &steep_overhang_hole, bool reverse_thin_wall_hole)
+ bool &steep_overhang_contour, bool &steep_overhang_hole)
{
// loops is an arrayref of ::Loop objects
// turn each one into an ExtrusionLoop object
@@ -285,24 +287,17 @@ static ExtrusionEntityCollection traverse_loops(const PerimeterGenerator &perime
} else {
const PerimeterGeneratorLoop &loop = loops[idx.first];
assert(thin_walls.empty());
- const bool reverse_children_thin_wall_hole = loops.size() == 1 && loop.is_contour && loop.children.size() == 1 &&
- (!loop.children.front().is_contour) && loop.children.front().children.empty();
- ExtrusionEntityCollection children = traverse_loops(perimeter_generator, loop.children, thin_walls, steep_overhang_contour,
- steep_overhang_hole, reverse_children_thin_wall_hole);
+ ExtrusionEntityCollection children = traverse_loops(perimeter_generator, loop.children, thin_walls, steep_overhang_contour, steep_overhang_hole);
out.entities.reserve(out.entities.size() + children.entities.size() + 1);
ExtrusionLoop *eloop = static_cast(coll.entities[idx.first]);
coll.entities[idx.first] = nullptr;
- if ((perimeter_generator.config->wall_direction == WallDirection::CounterClockwise) == (loop.is_contour || reverse_thin_wall_hole))
+ // Orca: holes run against the contour, except thin wall holes, which run with it.
+ if ((perimeter_generator.config->wall_direction == WallDirection::CounterClockwise) == (loop.is_contour || loop.is_thin_wall_hole))
eloop->make_counter_clockwise();
else
eloop->make_clockwise();
- // Orca: Reverse print order for thin wall holes.
- if (reverse_thin_wall_hole) {
- std::reverse(out.entities.begin(), out.entities.end());
- }
-
eloop->inset_idx = loop.depth;
if (loop.is_contour) {
out.append(std::move(children.entities));
@@ -1499,6 +1494,8 @@ void PerimeterGenerator::process_classic()
coord_t min_spacing = coord_t(perimeter_spacing * (1 - INSET_OVERLAP_TOLERANCE));
coord_t ext_min_spacing = coord_t(ext_perimeter_spacing * (1 - INSET_OVERLAP_TOLERANCE));
bool has_gap_fill = this->config->gap_infill_speed.get_at(get_extruder_index(*print_config, this->config->outer_wall_filament_id - 1)) > 0;
+ // ORCA: Use the smaller width as the lower bound to avoid overestimating safe overlap
+ const double gap_fill_min_width = 0.2 * std::min(perimeter_width, ext_perimeter_width) * (1 - INSET_OVERLAP_TOLERANCE);
// BBS: this flow is for smaller external perimeter for small area
coord_t ext_min_spacing_smaller = coord_t(ext_perimeter_spacing * (1 - SMALLER_EXT_INSET_OVERLAP_TOLERANCE));
@@ -1693,6 +1690,15 @@ void PerimeterGenerator::process_classic()
last = std::move(offsets);
+ // ORCA: a thin wall hole has no room for gap fill or an inner wall anywhere beside its outer wall.
+ if (i == 0 && ! holes[0].empty()) {
+ const float room = float(0.5 * (ext_perimeter_spacing2 + gap_fill_min_width));
+ const Polygons reach = to_polygons(offset2_ex(last, -room, room + float(SCALED_EPSILON)));
+ for (PerimeterGeneratorLoop &hole : holes[0])
+ hole.is_thin_wall_hole = intersection_pl(Polylines{ hole.polygon.split_at_first_point() },
+ ClipperUtils::clip_clipper_polygons_with_subject_bbox(reach, get_extents(hole.polygon).inflated(SCALED_EPSILON))).empty();
+ }
+
//BBS: refer to superslicer
//store surface for top infill if only_one_wall_top
if (i == 0 && i!=loop_number && only_one_wall_top && !surface.is_bridge() && this->upper_slices != NULL) {
@@ -1819,7 +1825,7 @@ void PerimeterGenerator::process_classic()
steep_overhang_contour = true;
steep_overhang_hole = true;
}
- ExtrusionEntityCollection entities = traverse_loops(*this, contours.front(), thin_walls, steep_overhang_contour, steep_overhang_hole, false);
+ ExtrusionEntityCollection entities = traverse_loops(*this, contours.front(), thin_walls, steep_overhang_contour, steep_overhang_hole);
// All walls are counter-clockwise initially, so we don't need to reorient it if that's what we want
if (config->overhang_reverse) {
reorient_perimeters(entities, steep_overhang_contour, steep_overhang_hole,
@@ -1946,8 +1952,7 @@ void PerimeterGenerator::process_classic()
// fill gaps
if (! gaps.empty()) { // collapse
- // ORCA: Use the smaller width as the lower bound to avoid overestimating safe overlap
- double min = 0.2 * std::min(perimeter_width, ext_perimeter_width) * (1 - INSET_OVERLAP_TOLERANCE);
+ double min = gap_fill_min_width;
double max = 2. * perimeter_spacing;
ExPolygons gaps_ex = diff_ex(
//FIXME offset2 would be enough and cheaper.
diff --git a/src/slic3r/GUI/3DScene.cpp b/src/slic3r/GUI/3DScene.cpp
index 597b960710..77010cbf9d 100644
--- a/src/slic3r/GUI/3DScene.cpp
+++ b/src/slic3r/GUI/3DScene.cpp
@@ -310,6 +310,7 @@ GLVolume::GLVolume(float r, float g, float b, float a)
, force_native_color(false)
, force_neutral_color(false)
, force_sinking_contours(false)
+ , depth_bias(false)
, picking(false)
, tverts_range(0, size_t(-1))
{
@@ -1332,11 +1333,17 @@ void GLVolumeCollection::render(GLVolumeCollection::ERenderType type,
shader->set_uniform("projection_matrix", projection_matrix);
const Matrix3d view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3) * model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose();
shader->set_uniform("view_normal_matrix", view_normal_matrix);
+ if (volume.first->depth_bias) {
+ glsafe(::glEnable(GL_POLYGON_OFFSET_FILL));
+ glsafe(::glPolygonOffset(1.0f, 1.0f));
+ }
//BBS: add outline related logic
if (volume.first->selected && shader_can_outline && GUI::wxGetApp().show_outline())
volume.first->render_with_outline(cnv_size);
else
volume.first->render();
+ if (volume.first->depth_bias)
+ glsafe(::glDisable(GL_POLYGON_OFFSET_FILL));
#if ENABLE_ENVIRONMENT_MAP
if (use_environment_texture)
diff --git a/src/slic3r/GUI/3DScene.hpp b/src/slic3r/GUI/3DScene.hpp
index 663979690f..664cd109cf 100644
--- a/src/slic3r/GUI/3DScene.hpp
+++ b/src/slic3r/GUI/3DScene.hpp
@@ -232,6 +232,10 @@ public:
bool force_neutral_color : 1;
// Whether or not to force rendering of sinking contours
bool force_sinking_contours : 1;
+ // Orca: draw this volume with a positive depth bias (glPolygonOffset, pushed away from the
+ // camera), so on a surface it shares with another volume the other volume wins the depth
+ // test instead of z-fighting it
+ bool depth_bias : 1;
// Is render for picking
bool picking : 1;
// slice error
diff --git a/src/slic3r/GUI/CAD/DesignCanvas.cpp b/src/slic3r/GUI/CAD/DesignCanvas.cpp
index 0e7296ccd4..0489df35c1 100644
--- a/src/slic3r/GUI/CAD/DesignCanvas.cpp
+++ b/src/slic3r/GUI/CAD/DesignCanvas.cpp
@@ -8,6 +8,7 @@
#include "slic3r/GUI/Camera.hpp" // N: look down the sketch plane normal
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Plater.hpp"
+#include "slic3r/GUI/PartPlate.hpp" // the current plate's origin: the first view's offset
#include "slic3r/GUI/ImGuiWrapper.hpp"
#include "slic3r/GUI/GLToolbar.hpp"
#include "slic3r/GUI/Event.hpp"
@@ -58,6 +59,16 @@ extern wxPopupWindow* wxCurrentPopupWindow;
namespace Slic3r {
namespace GUI {
+// The Design tab's iso view is the CAD one: a true isometric from the front-right corner, which
+// shows the navigator's Front, Right and Top faces. Camera's own "iso" looks from the front-left
+// at 45 degrees and stays Prepare's and the thumbnails'.
+static void select_iso_view(Camera& camera)
+{
+ const Vec3d target = camera.get_target();
+ camera.look_at(target + camera.get_distance() * Vec3d(1., -1., 1.).normalized(), target, Vec3d::UnitZ());
+ camera.auto_type(Camera::EType::Perspective);
+}
+
DesignCanvas::DesignCanvas(wxWindow* parent)
: wxPanel()
{
@@ -91,7 +102,7 @@ DesignCanvas::DesignCanvas(wxWindow* parent)
m_canvas->enable_plate_chrome(false);
m_canvas->enable_labels(false);
m_canvas->enable_sinking_contours(false); // they would be sliced from the plater's meshes
- m_canvas->set_axes_at_bed_center(true); // triad at bed centre = modeling origin
+ m_canvas->set_design_canvas(true); // home-position bed, triad and CAD grid at the modeling origin
m_canvas->set_design_sketch_tool(&m_sketch_tool);
m_sketch_tool.on_commit = [this](const SketchProfile& prof, const SketchPlane& pl) {
@@ -190,8 +201,13 @@ DesignCanvas::DesignCanvas(wxWindow* parent)
refresh_bed();
// The view this canvas opens on. Built lazily, on the way into the Design tab, so this
- // is the view the user is looking at right now.
+ // is the view the user is looking at right now — moved off the current plate onto the
+ // Design bed, which stays at the printer bed's home whichever plate is current — turned to
+ // the iso view Home returns to, keeping its target and zoom.
m_parked_camera = wxGetApp().plater()->get_camera();
+ if (PartPlate* plate = wxGetApp().plater()->get_partplate_list().get_curr_plate())
+ m_parked_camera.translate_world(-plate->get_origin());
+ select_iso_view(m_parked_camera);
// Before any of this class's own Binds below: wx calls dynamically bound handlers in
// reverse order of binding, and GLCanvas3D swallows several events without skipping them —
@@ -345,7 +361,7 @@ void DesignCanvas::repaint_now()
m_canvas_widget->Update(); // service the pending paint immediately (a modal popup owns the loop)
}
-void DesignCanvas::reload(bool keep_view)
+void DesignCanvas::reload()
{
m_canvas->reset_volumes();
@@ -378,21 +394,19 @@ void DesignCanvas::reload(bool keep_view)
v->set_color(c);
}
} else if (obj_idx == 1) {
- // The ghost is normally a faint blue overlay on the visible body. In preview-only
- // mode it IS the result (base bodies hidden), so render it opaque so it reads as a
- // finished solid rather than a see-through hint.
+ // The ghost is the whole resulting model, normally drawn as a faint blue overlay on
+ // the visible bodies. Every face the feature leaves alone is in both, at the same
+ // depth, so the ghost is drawn with a depth bias: the bodies win on those faces instead
+ // of the two copies z-fighting, and the ghost shows only where the result reaches past
+ // the bodies. In preview-only mode it IS the result (base bodies hidden), so render it
+ // opaque so it reads as a finished solid rather than a see-through hint.
v->set_color(m_body_hidden ? ColorRGBA(0.40f, 0.82f, 1.0f, 1.0f) : ghost);
+ v->depth_bias = true;
}
}
- if (!keep_view) {
- if (m_first_frame && !m_model.objects.empty()) {
- m_canvas->select_view("iso");
- m_canvas->zoom_to_volumes();
- m_first_frame = false;
- }
- }
-
+ // The camera stays where the user left it, even for the first body: Home and a double-click
+ // fit on demand.
m_canvas->set_as_dirty();
if (m_canvas_widget)
m_canvas_widget->Refresh();
@@ -406,18 +420,21 @@ void DesignCanvas::set_bodies(const std::vector* body_meshes,
if (body_meshes == nullptr || body_meshes->empty()) { clear_mesh(); return; }
m_body_meshes = body_meshes;
+ m_sketch_tool.refresh_body_edges(); // of the bodies set_solid_pick() pointed the tool at
m_lit_faces = m_sketch_tool.selected_faces();
rebuild_bodies();
- reload(!m_first_frame);
+ reload();
}
void DesignCanvas::clear_mesh()
{
m_body_meshes = nullptr;
m_volumes.clear();
+ // No solid, so no edge lines, pick or hover either, as after a rebuild that leaves no body.
+ m_sketch_tool.set_solid_pick(nullptr, nullptr, nullptr, nullptr);
if (!m_model.objects.empty()) {
m_model.delete_object((size_t)0);
- reload(true);
+ reload();
}
}
@@ -431,14 +448,14 @@ void DesignCanvas::set_preview_mesh(const TriangleMesh& mesh)
obj->add_volume(mesh);
obj->add_instance();
- reload(true);
+ reload();
}
void DesignCanvas::clear_preview()
{
if (m_model.objects.size() > 1) {
m_model.delete_object((size_t)1);
- reload(true);
+ reload();
}
}
@@ -452,10 +469,23 @@ void DesignCanvas::fit_view()
}
}
+bool DesignCanvas::zoom_to_box(BoundingBoxf3 box)
+{
+ if (m_canvas == nullptr || !box.defined)
+ return false;
+ DesignSketchTool::pad_box(box);
+ m_canvas->zoom_to_box(box);
+ request_repaint();
+ return true;
+}
+
void DesignCanvas::set_view(const std::string& view_name)
{
if (m_canvas) {
- m_canvas->select_view(view_name);
+ if (view_name == "iso")
+ select_iso_view(wxGetApp().plater()->get_camera()); // the Design camera while the tab is shown
+ else
+ m_canvas->select_view(view_name);
m_canvas->zoom_to_volumes();
m_canvas->set_as_dirty();
if (m_canvas_widget)
@@ -470,13 +500,6 @@ void DesignCanvas::begin_sketch(const SketchPlane& plane, DesignSketchTool::Mode
if (m_canvas_widget) m_canvas_widget->Refresh();
}
-void DesignCanvas::set_sketch_plane(const SketchPlane& plane)
-{
- m_sketch_tool.set_plane(plane); // keeps the 2D entities; only the carrier plane changes
- if (m_canvas) m_canvas->set_as_dirty();
- if (m_canvas_widget) m_canvas_widget->Refresh();
-}
-
void DesignCanvas::edit_sketch(const std::vector& entities,
const std::vector& constraints,
const SketchPlane& plane)
@@ -550,6 +573,7 @@ void DesignCanvas::refresh_bed()
double printable_height = 100.0;
const auto* ph_opt = config->opt("printable_height");
if (ph_opt) printable_height = ph_opt->value;
+ // No position: the Design bed stays at the printer bed's home, whichever plate is current.
m_bed.set_shape(bed_shape_opt->values, printable_height, {}, {}, "", false); // mainline added extruder_areas/heights params
}
@@ -731,8 +755,6 @@ void DesignCanvas::clear_move_gizmo()
request_repaint();
}
-bool DesignCanvas::moving_body() const { return m_sketch_tool.moving_body(); }
-
void DesignCanvas::set_on_body_move_changed(std::function cb)
{
m_sketch_tool.on_body_move_changed = std::move(cb);
@@ -1025,6 +1047,11 @@ void DesignCanvas::set_on_datum_base_picked(std::function cb)
m_sketch_tool.on_datum_base_picked = std::move(cb);
}
+void DesignCanvas::set_selected_base(std::function cb)
+{
+ m_sketch_tool.selected_base = std::move(cb);
+}
+
void DesignCanvas::set_on_sketch_exit(std::function cb)
{
m_sketch_tool.on_exit = std::move(cb);
@@ -1047,15 +1074,18 @@ void DesignCanvas::set_on_context_menu(std::function cb)
return;
m_ctx_bound = true;
// Bound AFTER GLCanvas3D's own handlers, so this runs first and can consume the event.
- // It only consumes when it actually opens the offer; every other right-click still falls
- // through to the polyline-chain end and the move gizmo, which were there first.
- // Right-drag pans. Without remembering where the press landed, every pan ended by popping
- // the offer over wherever the camera stopped — the menu appearing as the reward for moving
- // the view. The offer is the release of a STATIONARY right-click (kCadRightClickDriftPx).
+ // It only consumes when it actually opens the offer.
+ // Right-drag may pan or orbit (Preferences > Control). Without remembering where the press
+ // landed, every such drag ended by popping the offer over wherever the camera stopped — the
+ // menu appearing as the reward for moving the view. A right-click is the release of a
+ // STATIONARY press (kCadRightClickDriftPx); only that reaches the sketch tool or the offer.
m_canvas_widget->Bind(wxEVT_RIGHT_DOWN, [this](wxMouseEvent& e) {
m_ctx_press = e.GetPosition();
m_ctx_travelled = false;
- e.Skip(); // the canvas still needs the press to seed the orbit
+ // Drop any press the tool still keeps (only a click's release takes it, so a pan's stays)
+ // before the canvas offers it this one, which ImGui may take instead.
+ m_sketch_tool.drop_right_click();
+ e.Skip(); // the canvas still needs the press to seed a pan or an orbit
});
m_canvas_widget->Bind(wxEVT_MOTION, [this](wxMouseEvent& e) {
if (e.RightIsDown()) {
@@ -1066,12 +1096,13 @@ void DesignCanvas::set_on_context_menu(std::function cb)
});
m_canvas_widget->Bind(wxEVT_RIGHT_UP, [this](wxMouseEvent& e) {
const wxPoint d = e.GetPosition() - m_ctx_press;
- // Always read-and-clear, even when another guard already rules the offer out, or a
- // terminator recorded under one condition would still be pending under the next.
- const bool terminated = m_sketch_tool.take_right_consumed();
- // Click, or navigation? A press that travelled orbited; one that did not, did not.
+ // Click, or navigation? A press that travelled panned or orbited; one that did not, did not.
const bool is_click = !m_ctx_travelled && std::max(std::abs(d.x), std::abs(d.y)) <= kCadRightClickDriftPx;
- if (m_on_context_menu && !terminated && !inline_busy() && is_click) {
+ // Ending a chain or abandoning an anchor uses the click up.
+ const bool terminated = is_click && m_canvas && m_sketch_tool.take_right_click(*m_canvas);
+ if (terminated)
+ m_canvas->set_as_dirty(); // drawn by the canvas's own RightUp (e.Skip below) or at idle
+ else if (m_on_context_menu && !inline_busy() && is_click) {
// The menu belongs to what you POINTED AT — and pointing happened at the PRESS, not
// at the release, so the raycast uses the press position. Within a 3 px budget the
// two are the same pixel in practice; using the press is what makes that a
@@ -1167,9 +1198,6 @@ void DesignCanvas::set_readout(const std::string& text)
if (m_canvas) m_canvas->set_as_dirty(); // drawn by the next frame (the tool feeds this from one)
}
-// Clear of the view cube and the two round view buttons, which own the bottom-left corner.
-static constexpr float kStatusHudLeftInset = 190.f;
-
void DesignCanvas::set_status_text(const wxString& text, const wxColour& colour)
{
if (text == m_status_hud_last && colour == m_status_hud_colour) return;
@@ -1204,8 +1232,11 @@ void DesignCanvas::render_hud()
ImGuiWrapper::pop_common_window_style();
};
if (!m_status_hud_last.IsEmpty()) {
+ // Past the view cube and the round view buttons, which own the bottom-left corner. Asked
+ // of the canvas, which lays them out: they follow the monitor's DPI on Windows, where `em`
+ // does not, so a fixed inset in `em` let them cover the start of the line at 150%.
// A sentence can be a sentence: it wraps to the room left of the readout chip.
- const float left = kStatusHudLeftInset * em;
+ const float left = m_canvas->get_canvas_toolbar_right() + margin;
const ImVec4 col = m_status_hud_colour.IsOk()
? ImVec4(m_status_hud_colour.Red() / 255.f, m_status_hud_colour.Green() / 255.f,
m_status_hud_colour.Blue() / 255.f, 1.f)
@@ -1241,7 +1272,7 @@ void DesignCanvas::sync_selected_faces()
if (m_body_meshes == nullptr || m_model.objects.empty())
return;
rebuild_bodies();
- reload(true);
+ reload();
});
}
@@ -1293,7 +1324,7 @@ void DesignCanvas::set_operand_bodies(int target_body, int tool_body)
if (m_hl_body_target == target_body && m_hl_body_tool == tool_body) return;
m_hl_body_target = target_body;
m_hl_body_tool = tool_body;
- reload(true); // recolours the body volumes
+ reload(); // recolours the body volumes
}
void DesignCanvas::set_highlight_sketches(std::vector> hl)
@@ -1306,7 +1337,7 @@ void DesignCanvas::set_body_translucent(bool on)
{
if (m_body_translucent == on) return;
m_body_translucent = on;
- reload(true); // re-applies object-0 alpha so the solid fades for the fillet preview
+ reload(); // re-applies object-0 alpha so the solid fades for the fillet preview
}
void DesignCanvas::set_xray_focus(int body)
@@ -1314,7 +1345,7 @@ void DesignCanvas::set_xray_focus(int body)
if (m_xray_focus == body) return;
m_xray_focus = body;
m_sketch_tool.set_pick_only_body(body);
- reload(true); // re-applies per-body alpha so the non-focused bodies fade
+ reload(); // re-applies per-body alpha so the non-focused bodies fade
}
void DesignCanvas::set_body_hidden(bool on)
@@ -1322,7 +1353,7 @@ void DesignCanvas::set_body_hidden(bool on)
if (m_body_hidden == on) return;
m_body_hidden = on;
m_sketch_tool.set_body_edges_hidden(on);
- reload(true); // hides/show base bodies + flips the ghost opaque/faint for preview-only mode
+ reload(); // hides/show base bodies + flips the ghost opaque/faint for preview-only mode
}
bool DesignCanvas::delete_selected_sketch_entities()
diff --git a/src/slic3r/GUI/CAD/DesignCanvas.hpp b/src/slic3r/GUI/CAD/DesignCanvas.hpp
index 9584550980..a7413dd370 100644
--- a/src/slic3r/GUI/CAD/DesignCanvas.hpp
+++ b/src/slic3r/GUI/CAD/DesignCanvas.hpp
@@ -3,6 +3,8 @@
#include
#include "libslic3r/Point.hpp"
+#include "libslic3r/BoundingBox.hpp"
+#include "libslic3r/BuildVolume.hpp"
#include "libslic3r/Color.hpp"
#include
#include
@@ -48,16 +50,26 @@ public:
// Multi-body display: one GLVolume per body, each coloured distinctly (per-body colour).
// `visible` (optional, indexed by body) hides bodies whose flag is false. `body_meshes` is kept
- // by address (a stable panel member) and read again whenever the selection changes.
+ // by address (a stable panel member) and read again whenever the selection changes. The
+ // bodies' edge lines are refreshed with them (DesignSketchTool::refresh_body_edges).
void set_bodies(const std::vector* body_meshes,
const std::vector& visible = {});
- void clear_mesh();
+ void clear_mesh(); // no bodies: drops their volumes and the tool's edge lines, pick and selection
void set_preview_mesh(const TriangleMesh& mesh);
void clear_preview();
void fit_view();
void set_view(const std::string& view_name);
+ // Frame `box` along the current view direction, as the canvas's Fit button frames a selection.
+ // False, the camera left alone, when there is nothing to frame: an undefined box.
+ bool zoom_to_box(BoundingBoxf3 box);
+ // Boxes to hand zoom_to_box: the selection and body faces (see DesignSketchTool).
+ BoundingBoxf3 selection_box() const { return m_sketch_tool.selection_box(); }
+ BoundingBoxf3 faces_box(std::vector> faces) const
+ {
+ return m_sketch_tool.faces_box(std::move(faces));
+ }
void begin_sketch(const SketchPlane& plane, DesignSketchTool::Mode mode);
// Re-open a committed entity sketch for full in-canvas editing (load geometry +
@@ -66,7 +78,6 @@ public:
const std::vector& constraints,
const SketchPlane& plane);
void set_sketch_tool(DesignSketchTool::Mode mode);
- void set_sketch_plane(const SketchPlane& plane); // re-plane the live sketch when a reference plane is clicked in 3D
void set_sketch_construction(bool c);
// Flip the sketch selection between construction and real geometry; returns the
// number of entities changed (0 = nothing selected, caller falls back to the mode).
@@ -83,6 +94,8 @@ public:
void finish_sketch();
bool is_sketching() const;
void refresh_bed(); // re-sync the bed to the current printer (call on tab activation)
+ // Centre of the Design bed: the printer bed at its home position, whichever plate is current.
+ Vec2d bed_center() const { return m_bed.build_volume().bed_center(); }
// The Camera is Plater-owned and shared with Prepare/Preview/Assemble; GLCanvas3D has no
// per-canvas camera, so every orbit here would otherwise overwrite what the editor tabs
// show. Exactly one of the two views is live at a time, so entering and leaving are the
@@ -157,7 +170,6 @@ public:
void begin_move_body(int body, const Vec3d& pivot, const Transform3d& base_xform,
double body_radius);
void clear_move_gizmo();
- bool moving_body() const;
void set_on_body_move_changed(std::function cb);
// Visual Fillet/Chamfer radius gizmo: when a solid edge is picked, anchor a radius arrow on
// it; drag/edit fire the radius callback. Returns false if no edge is currently picked.
@@ -234,6 +246,7 @@ public:
std::vector labels = {}); // clickable labelled reference planes
void clear_base_pick();
void set_on_datum_base_picked(std::function cb);
+ void set_selected_base(std::function cb); // the reference plane drawn selected, or -1
void set_on_sketch_exit(std::function cb); // Esc -> exit the tool
void set_on_sketch_exit_refused(std::function cb); // Esc declined: sketch has work
void set_on_sketch_notice(std::function cb); // tool refusals/side effects
@@ -380,7 +393,7 @@ public:
void repaint_now();
private:
- void reload(bool keep_view);
+ void reload();
void swap_camera(); // enter_viewport / leave_viewport, in the one direction they share
// Selected faces are filled by the canvas: each body's selected faces become a volume of their
@@ -415,7 +428,6 @@ private:
Camera m_parked_camera;
bool m_camera_swapped{false}; // guards a leave without an enter, and the reverse
Model m_model;
- bool m_first_frame{true};
int m_hl_body_target{-1};
int m_hl_body_tool{-1};
bool m_body_translucent{false};// fillet/chamfer preview → render the body see-through
diff --git a/src/slic3r/GUI/CAD/DesignOffer.hpp b/src/slic3r/GUI/CAD/DesignOffer.hpp
index 846a55d72f..2c2d40d5fc 100644
--- a/src/slic3r/GUI/CAD/DesignOffer.hpp
+++ b/src/slic3r/GUI/CAD/DesignOffer.hpp
@@ -102,7 +102,7 @@ static const bool kOfferRowFlat[] = {
};
static const OfferVerb kOfferVerbs[] = {
- {"sketch", L_CONTEXT("Sketch", "Design"), 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")},
+ {"sketch", L_CONTEXT("Sketch", "Design"), 0, "Shift+S", "key:S+S", L("Select a flat face or a reference plane to sketch on"), 0x00000403u, 0, 0, false, false, nullptr, "design_sketch", L("Sketch on the selected flat face or plane, or click one next")},
{"extrude", L_CONTEXT("Extrude", "Design"), 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_CONTEXT("Revolve", "Design"), 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_CONTEXT("Sweep", "Design"), 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")},
@@ -145,6 +145,7 @@ static const OfferVerb kOfferVerbs[] = {
{"rename", L_CONTEXT("Rename…", "Design"), 8, "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_CONTEXT("Delete Face", "Design"), 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_CONTEXT("Color", "Design"), 8, nullptr, "btn:colour", nullptr, 0x000001feu, 1, 0, false, false, nullptr, "color_palette", L("Set the selected body's display color")},
+ {"zoom_to", L_CONTEXT("Zoom to selection", "Design"), 8, nullptr, "btn:zoom_to", nullptr, 0x000041feu, 0, 0, false, false, nullptr, "design_zoom", L("Frame the selection in the view, keeping the view direction")},
{"delete", L_CONTEXT("Delete", "Design"), 7, "Del", "btn:delete", nullptr, 0x000f7c00u, 0, 0, false, false, nullptr, "design_delete", L("Delete what is selected")},
{"delete_body", L_CONTEXT("Delete Body", "Design"), 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_CONTEXT("Line", "Design"), 0, "L", "key:L", nullptr, 0x000f8000u, 0, 0, false, true, L("Line"), "design_line", L("Line — click start, then end")},
@@ -204,7 +205,7 @@ static const OfferVerb kOfferVerbs[] = {
{"sk_radius", L_CONTEXT("Radius / diameter…", "Design"), 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_CONTEXT("Angle / distance…", "Design"), 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;
+static const int kOfferVerbCount = 93;
}} // namespace Slic3r::GUI
diff --git a/src/slic3r/GUI/CAD/DesignPanel.cpp b/src/slic3r/GUI/CAD/DesignPanel.cpp
index e21fc18085..a98bdad3b4 100644
--- a/src/slic3r/GUI/CAD/DesignPanel.cpp
+++ b/src/slic3r/GUI/CAD/DesignPanel.cpp
@@ -9,6 +9,7 @@
#include "slic3r/GUI/GUI_Utils.hpp"
#include "slic3r/GUI/Widgets/Button.hpp"
#include "libslic3r/CAD/GeometryEngine.hpp" // face_by_index for face-extrude gizmo anchor
+#include "libslic3r/AppConfig.hpp" // design_* view preferences
#include "libslic3r/TriangleMesh.hpp" // mesh import: STL/OBJ -> indexed_triangle_set
#include "libslic3r/Format/OBJ.hpp"
#include "libslic3r/Format/bbs_3mf.hpp" // put_other_changes: mark the project dirty outside the undo stack
@@ -35,6 +36,7 @@
#include "libslic3r/Point.hpp"
#include
#include
+#include
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/Color.hpp"
#include "libslic3r/CAD/SketchSolver.hpp"
@@ -104,7 +106,6 @@
#include "libslic3r/Model.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Plater.hpp"
-#include "libslic3r/BuildVolume.hpp"
#include "slic3r/GUI/MainFrame.hpp"
#include "slic3r/GUI/GUI_ObjectList.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
@@ -275,7 +276,9 @@ static std::string commit_icon(bool bodies)
}
// The icons on each Feature tree and Bodies row (DesignRowList::Action::id).
-enum RowAction { RowEdit, RowMove, RowVisibility, RowDelete };
+enum RowAction { RowEdit, RowMove, RowVisibility, RowDelete, RowZoom };
+// The rows of the Feature tree's pinned block, above the features.
+static constexpr int kOriginRow = 0, kBedRow = 1;
// A row's eye shows the state the row is in; its tip names what a click does.
static DesignRowList::Action eye_action(bool shown)
@@ -283,6 +286,25 @@ static DesignRowList::Action eye_action(bool shown)
return { RowVisibility, shown ? "design_eye" : "design_eye_off", shown ? _L("Hide") : _L("Show") };
}
+// What the Origin row's eye and its menu item do.
+static wxString origin_toggle_text(bool shown)
+{
+ return shown ? _L("Hide reference planes and axes") : _L("Show reference planes and axes");
+}
+
+// What the Bed row's eye and its menu item do.
+static wxString bed_toggle_text(bool shown)
+{
+ return shown ? _L("Hide the printer bed and its plate grid")
+ : _L("Show the printer bed and its plate grid");
+}
+
+// What Sketch asks for while it waits for a plane (DesignPanel::start_sketch).
+static wxString sketch_plane_prompt()
+{
+ return _L("Click a reference plane or a flat face to sketch on it — Esc cancels");
+}
+
// Prepare outlines every numeric field (rounded, #4A4A51 on dark). wxSpinCtrlDouble is a
// native GTK control that cannot draw that, and Orca's own SpinInput is int-only — it would
// silently truncate a 2.5 mm radius. So the double spin keeps its arrows and validation and
@@ -471,22 +493,11 @@ DesignPanel::DesignPanel(wxWindow* parent)
m_construction->SetForegroundColour(dp_ctl_text());
auto select_tool = [this](DesignSketchTool::Mode mode, const wxString& hint) {
if (!m_viewport) return;
- if (!m_viewport->is_sketching()) {
- wxString on;
- const SketchPlane plane = sketch_plane_from_selection(on);
- m_viewport->begin_sketch(plane, mode);
- m_construction->SetValue(false); // a fresh session starts non-construction
- m_sketch_on = on; // shown with the tool hint, so the target is visible
- // The face has been CONSUMED as the sketch plane, so drop the pick. Leaving it live
- // meant the next Extrude saw a selected face and push/pulled it instead of extruding
- // the sketch just drawn — the same trap the imported-art path already guards against.
- if (m_sel_solid_face >= 0 || m_pick_face >= 0) {
- m_sel_solid_face = m_sel_solid_edge = m_sel_solid_body = -1;
- m_pick_face = m_pick_face_body = -1;
- }
- } else {
- m_viewport->set_sketch_tool(mode);
- }
+ // Sketch mode opens with its session (start_sketch_on_target); this covers a mode a
+ // failed Constrain left without one.
+ if (!m_viewport->is_sketching())
+ start_sketch_on_target(false);
+ m_viewport->set_sketch_tool(mode);
m_viewport->set_sketch_construction(m_construction->GetValue());
set_status(StatusKind::Info, m_sketch_on.IsEmpty() ? hint
: wxString::Format(_L("%s · plane: %s"), hint, m_sketch_on));
@@ -597,17 +608,19 @@ DesignPanel::DesignPanel(wxWindow* parent)
set_status(_L("Axonometric view, fitted"));
};
- // Commit to Plate and the bed toggle were mouse-only: a toolbar button and a checkbox with
- // no accelerator between them, so neither could be reached from the keyboard at all, nor by
- // anything driving the keyboard. Ctrl+Shift+P is Plate, Ctrl+Shift+B is Bed; neither
- // collides with Orca's own Ctrl+Shift+S (Save as) or Ctrl+Shift+G (Print plate).
+ // Commit to Plate and the bed and origin toggles are mouse targets — a toolbar button and the
+ // Feature tree's Bed and Origin rows, which never take the focus — so without an accelerator
+ // none could be reached from the keyboard, nor by anything driving the keyboard. Ctrl+Shift+P
+ // is Plate, Ctrl+Shift+B is Bed, Ctrl+Shift+O is Origin; none collides with Orca's own
+ // Ctrl+Shift+S (Save as) or Ctrl+Shift+G (Print plate).
m_keys_feature['P' | SC_SHIFT | SC_CTRL] = [this] { on_commit(); };
m_keys_feature['B' | SC_SHIFT | SC_CTRL] = [this] {
- if (!m_show_bed) return;
- const bool show = !m_show_bed->GetValue();
- m_show_bed->SetValue(show);
- if (m_viewport) m_viewport->set_show_bed(show);
- set_status(show ? _L("Bed shown") : _L("Bed hidden"));
+ toggle_bed();
+ set_status(m_show_bed ? _L("Bed shown") : _L("Bed hidden"));
+ };
+ m_keys_feature['O' | SC_SHIFT | SC_CTRL] = [this] {
+ toggle_origin();
+ set_status(m_show_origin ? _L("Reference planes shown") : _L("Reference planes hidden"));
};
// Flyout rows show the design_* glyphs as they are: drawn in Orca's icon grey (#949494), which
@@ -625,14 +638,13 @@ DesignPanel::DesignPanel(wxWindow* parent)
// the addresses are created inside the widget-building loops, so not building would silently
// delete 42 verbs from the offer while they still rendered. 7ih records the cleanup
// that lets the construction go away too.
- // What stays: the two doc-row imports (consumed by add_doc below) and the view controls,
- // which are chrome_only in the atlas and so have no offer row to fall back on.
- static const std::set kBarKeep = { "step", "mesh", "place", "section", "flip" };
+ // What stays: the view controls, which are chrome_only in the atlas and so have no offer row
+ // to fall back on, and Sketch, the group's lead. All four are built with the doc buttons.
+ static const std::set kBarKeep = { "place", "section", "flip" };
auto fadd = [&tb_slot](const char* id, wxWindow* w) {
if (kBarKeep.count(id) == 0) { w->Hide(); return; }
tb_slot[id].push_back(w);
};
- m_tb_feature->Add(caption(_L("FEATURES")), 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 8);
{
// Onshape-style FEATURE flyouts: same themed-DropDown pattern as the sketch toolbar
// (tinted glyphs, Body_14 measure, content-width popup) but each entry runs an
@@ -710,36 +722,10 @@ DesignPanel::DesignPanel(wxWindow* parent)
fadd(id, chev);
};
- auto* b_sketch = icon_btn("design_sketch", _L("Sketch"));
- // Sketch means a tool. Pressing it used to set the mode and then ask, unconditionally,
- // for the very thing the user had just done — click XZ, read "XZ plane selected, press
- // Sketch", press Sketch, and be told to click a reference plane. The plane was never
- // lost (m_ref_plane holds it and begin_sketch captures it when the first tool is armed);
- // the sentence was simply false, and with right-click excluded in sketch mode there was
- // no door to the tools at all, so the only way on was the toolbar this tab is retiring.
- std::function act_sketch = [this] {
- set_ui_mode(UiMode::Sketch);
- wxString where;
- const bool have_plane = sketch_plane_target(where);
- set_status(StatusKind::Info, have_plane
- ? wxString::Format(_L("Sketch plane: %s — pick a tool"), where)
- : _L("Click a face or a reference plane in the viewport, then a sketch tool"));
- if (m_sketch_hint) { // the card must agree with the status line, not argue with it
- m_sketch_hint->SetLabel(have_plane
- ? wxString::Format(_L("Sketch plane: %s.\nPick a tool, or press Menu for the list."), where)
- : _L("Click a face or a reference plane, then a sketch tool."));
- m_sketch_hint->Refresh();
- m_cards->Layout();
- }
- // Hand over the tools rather than naming them in a status line. CallAfter so the
- // mode change has settled before a modal menu takes the loop; the menu carries each
- // tool's shortcut, so pressing the key instead of picking a row costs nothing.
- if (have_plane)
- CallAfter([this] { show_offer_menu(offer_anchor()); });
- };
- b_sketch->Bind(wxEVT_BUTTON, [act_sketch](wxCommandEvent&) { act_sketch(); });
- m_keys_feature[SHIFT('S')] = act_sketch;
- fadd("sketch", b_sketch);
+ // Sketch opens a sketch on the face or plane already picked, or asks for one first
+ // (start_sketch): sketch mode is never entered without a plane under it. Its button is
+ // built with the doc buttons below.
+ m_keys_feature[SHIFT('S')] = [this] { start_sketch(); };
// Add material: every feature that grows new solid material — from a profile
// (extrude/revolve/sweep/loft), from a face (thicken) or from a line (rib).
feat_dropdown("material", "design_extrude", _L("Add material (extrude / revolve / sweep / loft / thicken / rib)"), {
@@ -1080,6 +1066,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
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(); };
+ m_verb_actions["btn:zoom_to"] = [this] { zoom_to_selection(); };
m_verb_actions["btn:delete"] = [this] { on_delete_feature(); };
// A row's double-click is Edit, so renaming needs a door of its own. The offer is this
// tab's only tool vocabulary and the row IS the object, so rename sits in the offer, in
@@ -1091,8 +1078,8 @@ DesignPanel::DesignPanel(wxWindow* parent)
// is editable now (CadBody::user_name), so the verb opens the editor there.
if (const int b = tree_body_selection(); b >= 0)
m_parts->begin_rename(b);
- else if (const int sel = tree_selection(); sel != wxNOT_FOUND)
- m_tree->begin_rename(sel);
+ else if (tree_selection() != wxNOT_FOUND)
+ m_tree->begin_rename(m_tree->selection());
else
set_status(_L("Select a feature, or a body, first — then rename it")); // not an error
};
@@ -1240,18 +1227,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
}, SHIFT('T')},
});
// Text / SVG insert tools live in the SKETCH toolbar (they produce 2D profiles =
- // sketches), not here. STEP stays in Features: it imports a whole B-rep solid.
- // Import STEP — standalone: a STEP comes in as a whole editable B-rep body, not a profile.
- auto* b_step = icon_btn("design_step", _L("Import STEP (editable B-rep solid)"));
- b_step->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_import_step(); });
- m_keys_feature[SHIFT('I')] = [this] { on_import_step(); };
- fadd("step", b_step);
- // Import mesh — same destination as STEP (an editable B-rep body), but the geometry has
- // to be reconstructed from triangles first (GeometryEngine::mesh_to_brep).
- auto* b_mesh = icon_btn("param_triangles", _L("Import mesh (STL/OBJ) as an editable B-rep solid"));
- b_mesh->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_import_mesh(); });
- m_keys_feature[SHIFT('M')] = [this] { on_import_mesh(); };
- fadd("mesh", b_mesh);
+ // sketches), not here. Import STEP / mesh are document actions: see the doc row.
auto* b_constrain = icon_btn("design_constrain", _L("Constrain selected sketch"));
b_constrain->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) {
on_begin_constrain();
@@ -1398,12 +1374,11 @@ DesignPanel::DesignPanel(wxWindow* parent)
// the sketch tools, not in the generic Features strip. Each places the art
// in-canvas, then commits via the Insert card's Confirm.
{
- // In Sketch MODE the art must land in the sketch — but begin_sketch() does not run
- // until the first tool is armed, so pressing Sketch and then Text would find no
- // session and commit a separate feature. Arm Select first: it starts the session on
- // the picked plane without drawing anything, so Text behaves the same whether or not
- // you had already drawn a line. (MODE vs SESSION — the distinction that has bitten
- // this panel before.)
+ // In Sketch MODE the art must land in the sketch. The mode opens with its session
+ // (start_sketch_on_target), but a failed Constrain can leave it without one, and Text
+ // would then find no session and commit a separate feature. Arm Select first: it
+ // starts the session without drawing anything. (MODE vs SESSION — the distinction
+ // that has bitten this panel before.)
auto ensure_sketch = [this, select_tool] {
if (m_ui_mode == UiMode::Sketch && m_viewport && !m_viewport->is_sketching())
select_tool(DesignSketchTool::Mode::Select,
@@ -1540,10 +1515,10 @@ DesignPanel::DesignPanel(wxWindow* parent)
}
m_viewport->set_sketch_construction(m_construction->GetValue()); });
// STAYS on the bar. Construction is not a tool, it is a persistent MODE — the same kind
- // of thing as the Bed checkbox — and the sketch bar is already shown only in Sketch mode,
- // so it appears exactly while it can apply. Hiding it left Q and the offer's Construction
- // row still toggling a checkbox nobody could see: you could not tell whether the next
- // line would be construction geometry. A stateful toggle has to show its state.
+ // of thing as the Feature tree's Bed row — and the sketch bar is already shown only in
+ // Sketch mode, so it appears exactly while it can apply. Hiding it left Q and the offer's
+ // Construction row still toggling a checkbox nobody could see: you could not tell whether
+ // the next line would be construction geometry. A stateful toggle has to show its state.
sadd_bar(m_construction);
add_sep(m_tb_sketch);
auto* b_del = icon_btn("design_delete", _L("Delete selected"));
@@ -1633,31 +1608,29 @@ DesignPanel::DesignPanel(wxWindow* parent)
auto add_doc = [this](ScalableButton* b) {
m_tb_doc->Add(b, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, 4); };
- auto* b_new = doc_btn("add", _L("New Design — clear the feature tree"));
+ auto* b_new = doc_btn("design_new", _L("New Design — clear the feature tree"));
b_new->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_new_design(); });
add_doc(b_new);
- add_doc(static_cast(tb_slot["step"][0])); // 2. Import STEP
- add_doc(static_cast(tb_slot["mesh"][0])); // 3. Import mesh
- auto* b_export = doc_btn("save", _L("Export STEP…"));
+ // Import STEP — a STEP comes in as a whole editable B-rep body, not a profile.
+ auto* b_step = doc_btn("design_step", _L("Import STEP (editable B-rep solid)"));
+ b_step->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_import_step(); });
+ m_keys_feature[SHIFT('I')] = [this] { on_import_step(); };
+ add_doc(b_step);
+ // Import mesh — same destination as STEP (an editable B-rep body), but the geometry has
+ // to be reconstructed from triangles first (GeometryEngine::mesh_to_brep).
+ auto* b_mesh = doc_btn("design_import_mesh", _L("Import mesh (STL/OBJ) as an editable B-rep solid"));
+ b_mesh->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_import_mesh(); });
+ m_keys_feature[SHIFT('M')] = [this] { on_import_mesh(); };
+ add_doc(b_mesh);
+ auto* b_export = doc_btn("design_export", _L("Export STEP…"));
b_export->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_export_step(); });
add_doc(b_export);
- // View option, not a document action: hide the printer bed to model without it. Lives in
- // this row because it must stay reachable with no tool open — a card would come and go.
- m_show_bed = new CheckBox(m_toolbar);
- m_show_bed->SetValue(true); // bed visible by default, as the tab opens today
- m_show_bed->SetToolTip(_L("Show the printer bed and its plate grid"));
- // wxEVT_TOGGLEBUTTON, NOT wxEVT_CHECKBOX: Orca's CheckBox derives from
- // wxBitmapToggleButton (Widgets/CheckBox.hpp), so a wxEVT_CHECKBOX handler never fires.
- // Read the control rather than the event so the state cannot disagree with the glyph.
- m_show_bed->Bind(wxEVT_TOGGLEBUTTON, [this](wxCommandEvent& e) {
- if (m_viewport) m_viewport->set_show_bed(m_show_bed->GetValue());
- e.Skip();
- });
- auto* bed_lbl = new wxStaticText(m_toolbar, wxID_ANY, _L("Bed"));
- bed_lbl->SetForegroundColour(dp_sec_text());
- m_tb_doc->Add(m_show_bed, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, 6);
- m_tb_doc->Add(bed_lbl, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, 4);
+ // Sketch leads the feature group. Momentary like the imports: start_sketch() may only ask
+ // for a plane, or refuse, and a teal active-tool state would outlive either.
+ auto* b_sketch = doc_btn("design_new_sketch", _L("Create a new sketch"));
+ b_sketch->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { start_sketch(); });
+ tb_slot["sketch"].push_back(b_sketch);
// These act on bodies / the view, so they ride in the feature group, in the slots
// the user assigned them (9, 11bis, 16).
@@ -3111,11 +3084,9 @@ DesignPanel::DesignPanel(wxWindow* parent)
// looking and pointing. A combo duplicated that decision somewhere the geometry could not
// see it, and once a face could be picked it went further and displayed a stale row that
// contradicted the real target. e1p.
- // Kept as a member, not a local: the card has to be able to STOP saying this. It asked
- // for a plane even when one had just been picked, directly contradicting the status line
- // two inches below it, which by then read "Sketching on XZ".
- m_sketch_hint = new wxStaticText(m_cards, wxID_ANY,
- _L("Click a face or a reference plane, then a sketch tool."));
+ // A member, not a local: it names the plane the session opened on, which is known only
+ // when it opens (start_sketch_on_target writes it), so the card and the status line agree.
+ m_sketch_hint = new wxStaticText(m_cards, wxID_ANY, _L("Pick a tool, or press Menu for the list."));
m_sketch_hint->SetForegroundColour(dp_sec_text());
m_box_sketch_session->Add(m_sketch_hint, 0, wxLEFT | wxRIGHT | wxTOP | wxBOTTOM, 12);
}
@@ -3159,11 +3130,34 @@ DesignPanel::DesignPanel(wxWindow* parent)
FromDIP(SidebarProps::ContentMargin()));
tree_inner->Add(new wxStaticLine(m_tree_box), 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP,
FromDIP(SidebarProps::TitlebarMargin()));
- // Sized to its rows, so a short history wastes no block, and scrolling past 9.
+ // The Origin and Bed switches sit on the card itself, unframed, above the features' own framed
+ // list: view switches, not history. A list of their own so they stay put while the features
+ // scroll, and a non-selectable one because selecting them would do nothing: only their eyes
+ // answer. 13 = the feature list's margin plus its frame, so the two lists' icons line up.
+ m_pinned = new DesignRowList(m_tree_box, 2, false, wxBORDER_NONE);
+ m_pinned->SetBackgroundColour(dp_panel_bg());
+ tree_inner->Add(m_pinned, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, 13);
+ // Sized to its rows, so a short history wastes no block, and scrolling past 9 features.
m_tree = new DesignRowList(m_tree_box, 9);
m_tree->SetBackgroundColour(dp_panel_bg());
tree_inner->Add(m_tree, 0, wxEXPAND | wxALL, 12);
+ m_pinned->on_action = [this](int row, int) {
+ if (row == kBedRow)
+ toggle_bed();
+ else if (row == kOriginRow)
+ toggle_origin();
+ };
+ // Exactly what the eye does, named.
+ m_pinned->on_menu = [this](int row, const wxPoint& screen) {
+ const bool bed = (row == kBedRow);
+ wxMenu menu;
+ const int id_toggle = wxWindow::NewControlId();
+ menu.Append(id_toggle, bed ? bed_toggle_text(m_show_bed) : origin_toggle_text(m_show_origin));
+ menu.Bind(wxEVT_MENU, [this, bed](wxCommandEvent&) { bed ? toggle_bed() : toggle_origin(); }, id_toggle);
+ m_pinned->PopupMenu(&menu, m_pinned->ScreenToClient(screen));
+ };
+
// Selecting a feature that leaves a body (Extrude, Fillet, Chamfer, Hole, ...) lights the
// faces it made in the viewport — the fillet's round, not the whole part it sits on.
m_tree->on_select = [this] {
@@ -3172,7 +3166,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
// "the target" — but ONLY when this tree has a selection. Each list notifies on every
// change, so clicking a body row runs apply_body_row, whose m_tree->unselect() fires
// THIS handler, which would otherwise clear the body row the user had just clicked.
- if (m_parts && tree_selection() != wxNOT_FOUND) m_parts->unselect();
+ if (m_parts && m_tree->selection() != wxNOT_FOUND) m_parts->unselect();
const int sel = tree_selection();
// Likewise a viewport pick, which would be drawn just like the feature's faces. Not while
// a card is open: the card reads that pick.
@@ -3194,14 +3188,17 @@ DesignPanel::DesignPanel(wxWindow* parent)
};
// Double-click a row = Edit, the same gesture that re-opens a committed sketch on the canvas.
- m_tree->on_activate = [this] { on_edit_feature(); };
+ m_tree->on_activate = [this] {
+ if (tree_selection() != wxNOT_FOUND) on_edit_feature();
+ };
- // The row's own Edit / Show-hide / Delete, on the row the click selected. The body list is
- // cleared here too, not left to on_select, which re-clicking the selected row does not run:
- // a body row still selected would be what on_toggle_visibility acts on.
- m_tree->on_action = [this](int, int id) {
+ // The row's own Zoom to selection / Edit / Show-hide / Delete, on the row the click selected.
+ // The body list is cleared here too, not left to on_select, which re-clicking the selected row
+ // does not run: a body row still selected would be what on_toggle_visibility acts on.
+ m_tree->on_action = [this](int row, int id) {
if (m_parts) m_parts->unselect();
switch (id) {
+ case RowZoom: zoom_to_feature(row); break;
case RowEdit: on_edit_feature(); break;
case RowVisibility: on_toggle_visibility(); break;
case RowDelete: on_delete_feature(); break;
@@ -3222,6 +3219,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
const int id_up = wxWindow::NewControlId();
const int id_down = wxWindow::NewControlId();
const int id_vis = wxWindow::NewControlId();
+ const int id_zoom = wxWindow::NewControlId();
const int id_art = wxWindow::NewControlId();
const int id_del = wxWindow::NewControlId();
menu.Append(id_rename, _L("Rename\tF2"));
@@ -3236,6 +3234,9 @@ DesignPanel::DesignPanel(wxWindow* parent)
menu.Append(id_up, _L("Move up"));
menu.Append(id_down, _L("Move down"));
menu.Append(id_vis, _L("Show / hide"));
+ // Offered where the row's own icon is: on a row with something to frame.
+ const bool zoom = can_zoom_to_feature(row);
+ if (zoom) menu.Append(id_zoom, _L("Zoom to selection"));
menu.AppendSeparator();
menu.Append(id_del, _L("Delete"));
menu.Bind(wxEVT_MENU, [this](wxCommandEvent&) { m_verb_actions["btn:rename"](); }, id_rename);
@@ -3246,6 +3247,8 @@ DesignPanel::DesignPanel(wxWindow* parent)
menu.Bind(wxEVT_MENU, [this](wxCommandEvent&) { on_delete_feature(); }, id_del);
if (art)
menu.Bind(wxEVT_MENU, [this, row](wxCommandEvent&) { on_transform_imported(row); }, id_art);
+ if (zoom)
+ menu.Bind(wxEVT_MENU, [this, row](wxCommandEvent&) { zoom_to_feature(row); }, id_zoom);
m_tree->PopupMenu(&menu, m_tree->ScreenToClient(screen));
};
@@ -3347,12 +3350,14 @@ DesignPanel::DesignPanel(wxWindow* parent)
set_status(StatusKind::Info, wxString::Format(_L("Body %d selected — right-click for what applies to it"), b + 1));
};
m_parts->on_select = [this, apply_body_row] { apply_body_row(tree_body_selection()); };
- // The row's own Move / Show-hide / Delete. apply_body_row runs unconditionally, as for the
- // menu below: a face picked in the viewport since the row was selected has moved
- // m_sel_solid_body, which is the body on_move_body and on_delete_body act on.
+ // The row's own Zoom to selection / Move / Show-hide / Delete. apply_body_row runs
+ // unconditionally, as for the menu below: a face picked in the viewport since the row was
+ // selected has moved m_sel_solid_body, which is the body on_move_body and on_delete_body act
+ // on.
m_parts->on_action = [this, apply_body_row](int row, int id) {
apply_body_row(row);
switch (id) {
+ case RowZoom: zoom_to_body(row); break;
case RowMove: on_move_body(); break;
case RowVisibility: on_toggle_visibility(); break;
case RowDelete: on_delete_body(); break;
@@ -3365,9 +3370,10 @@ DesignPanel::DesignPanel(wxWindow* parent)
// been promising this right-click since before it existed.
m_parts->on_menu = [this, apply_body_row](int row, const wxPoint& screen) {
apply_body_row(row); // unconditional — see above
- // Let the modal menu take the loop after this handler returns — same CallAfter as the
- // sketch path, which learned it the hard way.
- CallAfter([this, screen] { show_offer_menu(screen); });
+ // From inside the event, as the feature rows do, never CallAfter'd: wxGTK sends the
+ // context menu on the right PRESS, and a menu GTK pops after that event has finished
+ // has no trigger time, so the button's release closes it the moment it opens.
+ show_offer_menu(screen);
};
// Renaming a BODY names the body itself. It does NOT rename the feature that created it:
// an Extrude, a Cut and a Fillet all land on one body, so source_feature is one operation in
@@ -3699,8 +3705,6 @@ DesignPanel::DesignPanel(wxWindow* parent)
// One selection at a time: a pick replaces the Feature tree row. Not while a card is open:
// the feature being edited keeps its row while the card's picks are made.
if (level >= 1 && m_active == Tool::None && m_tree) m_tree->unselect();
- // A pick that fell through the move gizmo (clicked off the arrows) exits move mode.
- if (m_viewport->moving_body()) m_viewport->clear_move_gizmo();
// Remember which body + face/edge so Extrude / dress-up target the RIGHT body.
m_sel_solid_body = (level >= 1) ? body : -1;
m_sel_solid_face = (level == 2) ? face : -1; // 4 = Vertex: a corner is not its face
@@ -3712,8 +3716,29 @@ DesignPanel::DesignPanel(wxWindow* parent)
m_pick_face_body = (level >= 1) ? body : -1;
m_pick_face = (level >= 1) ? face : -1;
// Last pick wins: a leftover loop pick would block Extrude's face push/pull branch, so
- // Extrude would extrude a sketch instead of push/pulling the clicked face.
- if (level >= 1) m_viewport->clear_loop_pick();
+ // Extrude would extrude a sketch instead of push/pulling the clicked face. And a reference
+ // plane picked before stops being the one chosen, as a plane pick drops a face: otherwise it
+ // would come back as the sketch plane, and be drawn selected, once this pick is let go.
+ if (level >= 1) {
+ m_viewport->clear_loop_pick();
+ m_plane_picked = false;
+ }
+ // Sketch is waiting for a plane: a flat face is one. m_pick_face is the face the click
+ // landed on whether it took the face, one of its edges or a corner — the rule Sketch
+ // applies to a face picked beforehand. A curved face says why it was not taken; a swept
+ // body or a click that let go of the pick leaves the prompt up.
+ if (sketch_plane_pick_live()) {
+ wxString on;
+ if (level >= 1 && sketch_plane_target(on)) {
+ start_sketch_on_target(true);
+ return;
+ }
+ set_status(StatusKind::Info, level >= 1 && m_pick_face >= 0
+ ? _L("That face is not flat — click a flat face or a reference plane")
+ : sketch_plane_prompt());
+ m_status->Refresh();
+ return;
+ }
// Say what got picked. Without this the ONLY feedback is the viewport highlight, so a
// pick that registers but draws faintly is indistinguishable from one that never
// happened — which is precisely how this failure was reported and why it resisted
@@ -3909,7 +3934,9 @@ DesignPanel::DesignPanel(wxWindow* parent)
// A click on nothing drops a list row as it drops a pick. Not while a card is open: the
// feature being edited keeps its row.
m_viewport->set_on_empty_pick([this] {
- if (m_active == Tool::None && deselect_rows())
+ if (m_active != Tool::None) return;
+ const bool row = deselect_rows();
+ if (drop_plane_pick() || row)
set_status(StatusKind::Info, _L("Nothing selected"));
});
@@ -3987,28 +4014,37 @@ DesignPanel::DesignPanel(wxWindow* parent)
refresh_plane_labels();
refresh_preview(); // re-resolve the frame + move the gizmo/ghosts to the new base
} else {
- // Clicking a reference plane in 3D IS how a sketch plane is chosen now. Record it and,
- // when a session is already live, re-plane it immediately: begin_sketch captures the
- // plane at first-tool-pick, so without this the entities would stay on the old plane
- // while the committed feature landed on the new one.
+ // Clicking a reference plane in 3D IS how a sketch plane is chosen. The planes are
+ // only pickable outside a session (DesignSketchTool hit-tests them while no session
+ // is live), and the canvas reports the click after clearing the solid selection on the
+ // same miss, so the face pick is already empty and the sketch falls back to this plane.
if (base >= 0) {
m_ref_plane = base;
m_plane_picked = true; // chosen, not merely defaulted to
m_pick_face = m_pick_face_body = -1; // last pick wins: a plane beats a stale face
- if (m_viewport && m_viewport->is_sketching())
- m_viewport->set_sketch_plane(plane_from_choice(m_ref_plane));
+ }
+ if (base >= 0 && sketch_plane_pick_live()) {
+ start_sketch_on_target(true); // Sketch was waiting for exactly this
+ return;
}
const wxString nm = ref_plane_name(base);
- // Both halves named the TOOLBAR, which no longer carries either button: the tools
- // moved to the offer. Name the gesture that actually works in each mode, and say
- // what a plain click does, since the two are easy to confuse on a plane.
- set_status(StatusKind::Ok, m_ui_mode == UiMode::Sketch
- ? wxString::Format(_L("%s plane selected — right-click for the drawing tools"), nm)
- : wxString::Format(_L("%s plane selected — right-click to sketch on it, "
- "or click an object to select it"), nm));
+ // Name the gesture that works, and what a plain click does, since the two are easy to
+ // confuse on a plane.
+ set_status(StatusKind::Ok,
+ wxString::Format(_L("%s plane selected — right-click to sketch on it, "
+ "or click an object to select it"), nm));
m_status->Refresh();
}
});
+ // The reference plane drawn selected: the Plane card's base, or else the plane a sketch would go
+ // on — chosen, and not overridden by a picked face (sketch_plane_target's test, without its
+ // per-face OCCT lookup, as this is asked every frame).
+ m_viewport->set_selected_base([this] {
+ if (m_active == Tool::Plane)
+ return m_plane_base != nullptr && m_pl_faceA < 0 ? m_plane_base->GetSelection() : -1;
+ const bool face = (m_sel_solid_face >= 0 && m_sel_solid_body >= 0) || (m_pick_face >= 0 && m_pick_face_body >= 0);
+ return m_plane_picked && !face ? m_ref_plane : -1;
+ });
// Move-body gizmo (M5): each drag/edit reports the body's new translation. Store it as a
// display-only per-body transform and re-feed the moved meshes (the OCCT shape is untouched,
@@ -4144,13 +4180,8 @@ DesignPanel::DesignPanel(wxWindow* parent)
const int key = e.GetKeyCode();
const bool ctrl = e.ControlDown() || e.CmdDown();
const bool sketching = (m_ui_mode == UiMode::Sketch) && m_viewport && m_viewport->is_sketching();
- // Which key MAP applies is a question about the MODE, not about whether a session is
- // already running. Gating the sketch map on is_sketching() made it unreachable by
- // keyboard: is_sketching() only turns true inside select_tool()'s begin_sketch(), and
- // select_tool() is what the sketch keys call — so the first letter after entering
- // sketch mode fell through to the feature map, matched nothing (feature keys are
- // Shift+letter), and did nothing. The mouse worked only because the toolbar flyout
- // reaches select_tool() directly. That is why all 17 keys read as dead. 0ud.
+ // Which key MAP applies is a question about the MODE, not about the canvas tool:
+ // is_sketching() is true in Constrain too, which keeps the feature map. 0ud.
const bool sketch_mode = (m_ui_mode == UiMode::Sketch);
// Never steal editing keys from a focused text field or an open in-canvas value field —
// Delete/Ctrl+Z there must edit the text, not the model.
@@ -4416,7 +4447,13 @@ DesignPanel::DesignPanel(wxWindow* parent)
assert(dead.empty()); // debug builds stop here; release ships the log line
}
+ m_show_origin = wxGetApp().app_config->get_bool("design_show_origin");
+ m_show_bed = wxGetApp().app_config->get_bool("design_show_bed");
+ if (m_viewport) m_viewport->set_show_bed(m_show_bed);
set_ui_mode(UiMode::Feature);
+ // The Origin and Bed rows from the first paint: nothing else builds them before the first
+ // edit, project load or theme switch.
+ refresh_pinned();
build_phase("wiring");
BOOST_LOG_TRIVIAL(info) << "Design tab build: total " << build_clock.Time() << " ms";
}
@@ -4460,7 +4497,13 @@ void DesignPanel::apply_dof_status(int dof, bool ok, bool has_constraints)
void DesignPanel::set_ui_mode(UiMode m)
{
+ // A session's ✓/✗ and Esc are its own; a Move left open would answer them instead, unseen,
+ // since the gizmo is not drawn in a sketch.
+ if (m != UiMode::Feature) end_body_move(true);
m_ui_mode = m;
+ // Any mode change ends a plane choice under way; the refreshes below put the planes and the
+ // ✓/✗ right for it.
+ m_choosing_sketch_plane = false;
if (m != UiMode::Sketch) m_sketch_on.clear(); // no stale "on the picked face" on the next hint
// A committed loop picked before the session means nothing to the sketch map, but it would
// still count as a selection and swallow the first Esc. The double-click that opens a sketch
@@ -4507,9 +4550,8 @@ void DesignPanel::set_ui_mode(UiMode m)
m_form->FitInside();
}
update_action_bar(); // Sketch/Constrain modes show the unified ✓/✗; Feature idle hides it
- // The origin planes follow the mode: entering Sketch offers them even when a body exists,
- // leaving it takes them back. Without this they would only refresh on the next tree
- // rebuild, which is not an event that happens when you merely press Sketch.
+ // The reference planes follow the plane choice the mode change just ended
+ // (update_reference_planes); without this they would wait for the next tree rebuild.
update_reference_planes();
// Say where you are, in words, across the top of the viewport.
@@ -4519,8 +4561,8 @@ void DesignPanel::set_ui_mode(UiMode m)
// Seen on the rig: pick XY, arm Line, and the viewport is an empty grey field — no bed, no
// grid, no origin, nothing to judge a length or a direction against. The plate grid was
// carrying the ground reference for the whole tab. The banner already says where you are;
- // taking the floor away as well only made the sketch harder to draw. The Bed checkbox is the
- // one thing that governs the bed, in every mode.
+ // taking the floor away as well only made the sketch harder to draw. The Feature tree's Bed
+ // row is the one thing that governs the bed, in every mode.
if (m_sketch_banner != nullptr) {
const bool sketching = (m == UiMode::Sketch);
if (sketching && m_sketch_banner_txt != nullptr)
@@ -4546,7 +4588,9 @@ void DesignPanel::set_status_ok()
set_status(wxString::Format(_L("OK — %zu triangles"),
m_doc.display_mesh.its.indices.size()));
if (m_viewport != nullptr) {
- m_viewport->clear_move_gizmo(); // a recompute invalidates the gizmo's body centroid
+ // A recompute invalidates the gizmo's body centroid; a Move ends with it, keeping its pose.
+ if (body_move_pending()) end_body_move(true);
+ else m_viewport->clear_move_gizmo(); // the Transform card's, if up
rebuild_disp_meshes(); // apply per-body Move transforms to the display/pick meshes
// Point the solid-pick at the fresh body + TRANSFORMED pick mesh (stable address) + the
// per-body xform vector (for edge sampling). Resets the whole/face/edge selection, whose
@@ -4657,6 +4701,8 @@ void DesignPanel::on_add_text()
void DesignPanel::open_text_dialog(int feat)
{
if (m_text_dlg != nullptr) { m_text_dlg->Raise(); return; }
+ end_body_move(true);
+ end_sketch_plane_choice(); // the text goes on the plane picked before, not on the next click
m_text_editing = feat >= 0 && feat < int(m_doc.features.size()) && m_doc.features[feat].is_text();
m_text_feat = m_text_editing ? feat : -1;
m_text_face_body = -1;
@@ -5116,7 +5162,9 @@ void DesignPanel::add_imported_sketch(
// Show the Insert Confirm/Cancel card while the imported art is being placed/sized.
void DesignPanel::open_insert_card(const wxString& base_name)
{
+ end_body_move(true);
m_active = Tool::Insert;
+ end_sketch_plane_choice();
if (m_hdr_insert) m_hdr_insert->SetLabel(base_name);
wxSizer* s = m_cards->GetSizer();
s->Show(m_box_insert, true, true);
@@ -5161,6 +5209,8 @@ void DesignPanel::on_transform_imported(int feat_idx)
const CadFeature& f = m_doc.features[feat_idx];
if (f.imported_regions.empty())
return;
+ end_body_move(true); // the art's placement gizmo and ✓/✗ take over from a Move left open
+ end_sketch_plane_choice(); // ...and from a plane choice: Scale artwork reaches here directly
// In-canvas bbox handles (replaces the Move/Scale dialog): drag a corner to scale,
// the centre to move. Values stream back via set_on_imported_transform.
m_viewport->begin_imported_transform(feat_idx, f.imported_regions, f.plane,
@@ -5688,7 +5738,6 @@ void DesignPanel::on_add_transform()
// below does not — without this the body would keep showing the
// dragged pose after a Transform that failed to build.
m_xf_gizmo_body = -1;
- m_move_body = -1;
}
if (m_doc.bodies.empty()) {
set_status(StatusKind::Warning, _L("Transform needs a body — add or import one first"));
@@ -7247,7 +7296,7 @@ wxString DesignPanel::idle_hint() const
{
return m_doc.features.empty()
? _L("Nothing yet — import a STEP or a mesh from the toolbar,\n"
- "or click a reference plane and right-click it to start a sketch.")
+ "or press Sketch and click a reference plane to start a sketch.")
: _L("No solid yet — select a sketch and right-click it to Extrude.");
}
@@ -7275,7 +7324,7 @@ void DesignPanel::on_tab_shown()
if (m_viewport) m_viewport->refresh_bed();
hydrate_from_model();
- update_reference_planes(); // entering the Design tab: show the XY/XZ/YZ planes if no object yet
+ update_reference_planes(); // entering the Design tab
if (show_clock.Time() > 100) // a slow first show is what users report; the usual one is not news
BOOST_LOG_TRIVIAL(info) << "Design tab shown: bed, project recipe and planes in " << show_clock.Time() << " ms";
m_laid_out = true;
@@ -7336,6 +7385,7 @@ void DesignPanel::on_sys_color_changed()
wxGetApp().UpdateDarkUIWin(this);
refresh_icons();
refresh_tree(); // the rows carry their own text colours
+ refresh_pinned();
Refresh();
}
@@ -7354,8 +7404,12 @@ void DesignPanel::hydrate_from_model()
// that has none yet. A design in progress keeps its origin: its sketches have it baked into
// their planes, so moving it under them (a printer change between visits) would slide the
// datum planes off the geometry. A loaded project brings its own (load_recipe).
+ // The centre of the Design bed, not the plater's: the plater moves its bed onto the current
+ // plate, so on plate 2+ the origin would land one plate stride away from the bed drawn here.
+ // Re-synced first because the control socket calls this without on_tab_shown().
if (!m_doc.origin_from_recipe) {
- const Vec2d bc = plater->build_volume().bed_center();
+ m_viewport->refresh_bed();
+ const Vec2d bc = m_viewport->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).
@@ -7503,12 +7557,12 @@ void DesignPanel::refresh_tree()
// Preserve the selected row across the rebuild — set_rows() drops the selection, which made
// every edit/add feel like it "lost" the selection (and broke Edit/Move/Delete on the
// just-touched feature).
- const int keep = tree_selection();
+ const int keep = m_tree->selection();
// Datum/reference planes carry no solid; feed them to the viewport so they render as
// translucent rectangles (otherwise a Plane feature is invisible in the canvas).
refresh_datum_planes();
- update_reference_planes(); // body added/removed -> show/hide the XY/XZ/YZ origin planes
+ update_reference_planes();
std::vector rows;
rows.reserve(m_doc.features.size());
for (size_t fi = 0; fi < m_doc.features.size(); ++fi) {
@@ -7529,7 +7583,9 @@ void DesignPanel::refresh_tree()
row.colour = !f.enabled ? dp_item_dim()
: mate_conflict_reason(int(fi)) != nullptr ? wxColour(235, 110, 110)
: dp_item_text();
+ // A row with nothing to frame keeps a blank cell, so the icons stay in their columns.
row.actions = {
+ { RowZoom, can_zoom_to_feature(int(fi)) ? "design_zoom" : "", _L("Zoom to selection") },
{ RowEdit, "design_edit", _L("Edit") },
eye_action(f.enabled),
{ RowDelete, "design_delete", _L("Delete") },
@@ -7573,6 +7629,7 @@ void DesignPanel::refresh_parts()
// Hidden bodies are greyed and their eye is closed, so the state reads at a glance.
row.colour = vis ? dp_item_text() : dp_item_dim();
row.actions = {
+ { RowZoom, "design_zoom", _L("Zoom to selection") },
{ RowMove, "design_move", _L("Move") },
eye_action(vis),
{ RowDelete, "design_delete", _L("Delete") },
@@ -7605,12 +7662,22 @@ int DesignPanel::tree_body_selection() const
// if a row was selected.
bool DesignPanel::deselect_rows()
{
- const bool any = (m_tree && tree_selection() != wxNOT_FOUND) || tree_body_selection() >= 0;
+ const bool any = (m_tree && m_tree->selection() != wxNOT_FOUND) || tree_body_selection() >= 0;
if (m_tree) m_tree->unselect();
if (m_parts) m_parts->unselect();
return any;
}
+// A picked reference plane is a selection like a face: the next Sketch opens on it, so letting go
+// of everything has to let go of it too. The viewport asks for it every frame (set_selected_base).
+bool DesignPanel::drop_plane_pick()
+{
+ if (!m_plane_picked) return false;
+ m_plane_picked = false;
+ if (m_viewport) m_viewport->request_repaint();
+ return true;
+}
+
void DesignPanel::update_section_flip_btn()
{
if (m_section_flip_btn) m_section_flip_btn->Enable(m_section_on);
@@ -7725,23 +7792,108 @@ void DesignPanel::update_feature_highlight()
const int sel = tree_selection();
// Hidden while a feature card is open: the card's ghost and picks are what the view is about.
const bool wanted = sel >= 0 && sel < int(m_doc.features.size()) && m_active == Tool::None;
- if (wanted && (sel != m_hl_feature || m_hl_generation != m_doc.topo_generation)) {
+ // Re-sending the same faces is cheap: the viewport reuses what it has.
+ m_viewport->set_highlight_faces(wanted ? feature_faces(sel) : std::vector>{});
+}
+
+const std::vector>& DesignPanel::feature_faces(int f)
+{
+ if (f < 0 || f >= int(m_doc.features.size())) {
+ static const std::vector> none;
+ return none;
+ }
+ if (f != m_hl_feature || m_hl_generation != m_doc.topo_generation) {
std::vector> faces;
- const CadFeature& f = m_doc.features[sel];
- if (f.enabled && CadDocument::produces_body(f.type)) // the rest make no faces
- run_off_ui_thread(this, _L("Finding the feature's faces…"), [this, sel, &faces] {
+ const CadFeature& feat = m_doc.features[f];
+ if (feat.enabled && CadDocument::produces_body(feat.type)) // the rest make no faces
+ run_off_ui_thread(this, _L("Finding the feature's faces…"), [this, f, &faces] {
try {
- faces = m_doc.faces_made_by(sel);
+ faces = m_doc.faces_made_by(f);
} catch (...) {
faces.clear(); // a highlight is not worth an escaped exception
}
});
m_hl_faces = std::move(faces);
- m_hl_feature = sel;
+ m_hl_feature = f;
m_hl_generation = m_doc.topo_generation;
}
- // Re-sending the same faces is cheap: the viewport reuses what it has.
- m_viewport->set_highlight_faces(wanted ? m_hl_faces : std::vector>{});
+ return m_hl_faces;
+}
+
+bool DesignPanel::can_zoom_to_feature(int f) const
+{
+ if (f < 0 || f >= int(m_doc.features.size()))
+ return false;
+ const CadFeature& feat = m_doc.features[f];
+ return feat.type == CadFeatureType::Sketch || (feat.enabled && CadDocument::produces_body(feat.type));
+}
+
+void DesignPanel::zoom_to_feature(int f)
+{
+ // A rebuild's busy loop runs queued clicks while its worker owns the document.
+ if (s_doc_worker_busy.load() > 0) {
+ set_status(StatusKind::Info, _L("The model is being rebuilt — zoom again when it finishes"));
+ return;
+ }
+ if (m_viewport == nullptr || !can_zoom_to_feature(f))
+ return;
+ const CadFeature& feat = m_doc.features[f];
+ const wxString name = wxString::FromUTF8(feat.name); // before finding faces runs the event loop
+ BoundingBoxf3 box;
+ if (feat.type == CadFeatureType::Sketch) {
+ // Whichever geometry the sketch keeps, drawn or not: a sketch an Extrude consumed is
+ // still where the Extrude starts.
+ const auto add = [&box, &feat](const Vec2d& p) { box.merge(feat.plane.to_world(p)); };
+ if (!feat.entities.empty()) {
+ box = DesignSketchTool::sketch_box(feat.entities, feat.plane);
+ } else if (!feat.imported_regions.empty()) {
+ for (const auto& region : transform_regions(feat.imported_regions, feat.import_offset,
+ feat.import_scale_x, feat.import_scale_y))
+ for (const auto& contour : region)
+ for (const Vec2d& p : contour)
+ add(p);
+ } else if (!feat.profile.points.empty()) {
+ for (const Vec2d& p : feat.profile.points)
+ add(p);
+ } else {
+ // The legacy shape, centred on the plane origin (CadDocument::build_sketch_wire).
+ const Vec2d h = feat.shape == SketchShape::Circle ? Vec2d(feat.radius, feat.radius)
+ : Vec2d(feat.width, feat.height) * 0.5;
+ for (const Vec2d& p : { Vec2d(-h.x(), -h.y()), Vec2d(h.x(), -h.y()), Vec2d(h.x(), h.y()), Vec2d(-h.x(), h.y()) })
+ add(p);
+ }
+ } else {
+ box = m_viewport->faces_box(feature_faces(f));
+ }
+ if (!m_viewport->zoom_to_box(box))
+ set_status(StatusKind::Info, wxString::Format(_L("%s has nothing on show to zoom to"), name));
+}
+
+void DesignPanel::zoom_to_body(int b)
+{
+ if (m_viewport == nullptr)
+ return;
+ // From the body's display mesh, Move applied, rather than the selection: the viewport selects
+ // no hidden body (DesignSketchTool::select_body). An empty mesh's box still reads defined.
+ BoundingBoxf3 box;
+ if (b >= 0 && b < int(m_disp_body_meshes.size()) && !m_disp_body_meshes[b].empty())
+ box = m_disp_body_meshes[b].bounding_box();
+ if (!m_viewport->zoom_to_box(box))
+ set_status(StatusKind::Error, _L("That body has no display mesh yet — recompute first"));
+}
+
+void DesignPanel::zoom_to_selection()
+{
+ if (m_viewport == nullptr)
+ return;
+ // A body, as its row frames it; the kind has checked the index.
+ const OfferSel kind = OfferSel(offer_selection_kind());
+ if (kind == OfferSel::BodySolid || kind == OfferSel::BodySheet) {
+ zoom_to_body(m_sel_solid_body);
+ return;
+ }
+ if (!m_viewport->zoom_to_box(m_viewport->selection_box()))
+ set_status(StatusKind::Info, _L("Select something to zoom to"));
}
// Boolean (combine bodies) — one gate for every door onto the tool. A body-body operation
@@ -7761,6 +7913,12 @@ void DesignPanel::on_move_body()
{
const int b = m_sel_solid_body;
if (m_viewport == nullptr) return;
+ // Whatever is open owns the canvas and the ✓/✗, so a Move armed under it could not be left.
+ // The Bodies list stays clickable meanwhile, hence the check here.
+ if (m_active != Tool::None || m_ui_mode != UiMode::Feature || m_text_dlg != nullptr) {
+ set_status(StatusKind::Info, _L("Finish or cancel what is open first"));
+ return;
+ }
if (b < 0 || b >= int(m_doc.display_body_meshes.size())) {
// Never fail silently here: the caller gates on bodies.size() while this needs a
// tessellated per-body mesh, and when those disagreed the click did nothing at all.
@@ -7768,6 +7926,14 @@ void DesignPanel::on_move_body()
: _L("That body has no display mesh yet — recompute first"));
return;
}
+ const wxString hint = _L("Drag the arrows to move, the rings to rotate — then Confirm (Esc cancels)");
+ // Already moving it: Cancel still goes back to where it began. Another body re-arms below,
+ // and the one being moved keeps its placement.
+ if (body_move_pending() && b == m_move_body) {
+ set_status(StatusKind::Info, hint);
+ return;
+ }
+ end_sketch_plane_choice(); // the gizmo takes the clicks a plane choice was waiting for
sync_body_xform();
// Delta gizmo: pivot at the body's CURRENT world centroid; the tool composes the drag deltas
// onto its current pose, so move + rotate both work (incl. on an already place-on-face'd body).
@@ -7788,7 +7954,25 @@ void DesignPanel::on_move_body()
if (m_move_angle) m_move_angle->SetValue(0.0);
show_move_card(true);
update_action_bar(); // surface the unified ✓/✗ while moving
- set_status(StatusKind::Info, _L("Drag the arrows to move, the rings to rotate — then Confirm (Esc cancels)"));
+ set_status(StatusKind::Info, hint);
+}
+
+// The one way out of the Move button's session, so the gizmo, the Move / Rotate card and the ✓/✗
+// go together. keep = true leaves the pose on screen, as starting another edit does; only Esc and
+// Cancel put the body back. A no-op when no Move is up.
+void DesignPanel::end_body_move(bool keep)
+{
+ if (!body_move_pending()) return;
+ if (!keep) {
+ sync_body_xform();
+ if (m_move_body < int(m_body_xform.size()))
+ m_body_xform[m_move_body] = m_move_prev;
+ feed_bodies(); // re-render the reverted placement
+ }
+ m_viewport->clear_move_gizmo();
+ m_move_body = -1;
+ show_move_card(false);
+ update_action_bar();
}
// Transform card (add mode): arm the same move gizmo on the card's body so the geometry-first
@@ -7809,8 +7993,6 @@ void DesignPanel::arm_transform_gizmo()
if (m_viewport) m_viewport->begin_move_body(b, pivot, base, radius);
m_xf_gizmo_body = b;
m_xf_gizmo_base = base;
- m_move_body = b; // the existing revert paths key off these two
- m_move_prev = base;
// Write the pivot into the card so the parametric feature reproduces what was dragged;
// dx/dy/dz and angle start at zero (the gizmo reports deltas from this pose).
if (m_xf_pivot_x) m_xf_pivot_x->SetValue(pivot.x());
@@ -7869,6 +8051,7 @@ bool DesignPanel::place_on_face()
}
const TopoDS_Face face = GeometryEngine::face_by_index(m_doc.bodies[b].shape, m_sel_solid_face);
if (face.IsNull()) return false;
+ end_sketch_plane_choice(); // the body moves; a sketch was not started
sync_body_xform();
const Transform3d old_x = m_body_xform[b];
// Outward face normal in the body's CURRENT displayed orientation.
@@ -7902,8 +8085,7 @@ void DesignPanel::set_tree_selection(int row)
// The selection (the solid pick, the hit face and the committed-loop pick) names bodies, faces and
// features by index, so replacing or renumbering the feature list (undo/redo, New Design, load,
-// delete, reorder) drops it, the viewport's highlights with it. The solid highlight too: a rebuild
-// that leaves no body never reaches set_solid_pick. The callers repaint.
+// delete, reorder) drops it, the viewport's highlights with it. The callers repaint.
void DesignPanel::drop_selection()
{
if (m_viewport != nullptr)
@@ -7930,7 +8112,7 @@ void DesignPanel::drop_solid_pick()
bool DesignPanel::begin_renumber()
{
if (m_ui_mode != UiMode::Feature || m_text_dlg != nullptr || m_active == Tool::Insert
- || (m_active == Tool::None && m_viewport != nullptr && m_viewport->moving_body())) {
+ || body_move_pending()) {
set_status(StatusKind::Info, _L("Finish or cancel what is open first"));
return false;
}
@@ -7942,6 +8124,9 @@ bool DesignPanel::begin_renumber()
void DesignPanel::after_tree_edit(bool ok)
{
+ // Any edit of the history (undo, delete, show/hide, a socket call...) writes the status line
+ // over the plane prompt, so it ends the choice rather than leave it waiting unannounced.
+ end_sketch_plane_choice();
update_undo_redo_buttons();
refresh_tree();
refresh_variables();
@@ -7993,13 +8178,12 @@ void DesignPanel::on_new_design()
void DesignPanel::clear_document()
{
tool_cancel(); // leave any active tool / sketch / constrain cleanly
+ end_body_move(true); // ...and a Move, which tool_cancel leaves while a value is pending
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);
drop_selection();
m_edit_index = -1;
- m_move_body = -1;
- show_move_card(false);
m_body_xform.clear();
if (m_viewport) { m_viewport->clear_move_gizmo(); m_viewport->clear_mesh(); }
after_tree_edit(true); // rebuild the (now empty) tree + clear the viewport
@@ -8083,6 +8267,11 @@ void DesignPanel::on_toggle_visibility()
return;
}
+ // Hiding or showing a feature can renumber the bodies, and the Move holds a body index.
+ if (body_move_pending()) {
+ set_status(StatusKind::Info, _L("Finish or cancel what is open first"));
+ return;
+ }
int sel = tree_selection();
if (sel == wxNOT_FOUND || sel >= int(m_doc.features.size())) {
set_status(StatusKind::Error, _L("Select a feature in the tree first"));
@@ -8099,8 +8288,18 @@ void DesignPanel::on_toggle_visibility()
refresh_tree(); // greys the row
set_tree_selection(sel); // keep the toggled feature selected
if (m_viewport != nullptr) {
- if (m_doc.display_mesh.its.indices.empty()) m_viewport->clear_mesh();
- else feed_bodies();
+ if (m_doc.display_mesh.its.indices.empty()) {
+ m_viewport->clear_mesh();
+ } else {
+ // Not set_status_ok(), which would take the gizmo from an open Transform card.
+ feed_bodies();
+ // Hiding or showing a feature rebuilds the bodies and can renumber them, so the pick is
+ // re-pointed and its selection dropped. That also brings picking back for a body shown
+ // after everything was hidden (clear_mesh() dropped it).
+ m_viewport->set_solid_pick(&m_doc.bodies, &m_disp_pick_mesh,
+ &m_doc.display_tri_face, &m_doc.display_tri_body,
+ &m_body_visible, &m_body_xform);
+ }
}
sync_sketch_display(); // skips the hidden sketch + direct-renders
set_status(StatusKind::Info, shown ? _L("Feature shown") : _L("Feature hidden"));
@@ -10008,6 +10207,7 @@ void DesignPanel::on_commit()
// is shown on screen, not the pre-feature solid. (confirm_tool() applies + closes.)
if (m_active != Tool::None)
confirm_tool();
+ end_sketch_plane_choice(); // the bodies leave for the plate; a sketch was not started
if (m_doc.display_mesh.its.indices.empty()) {
set_status(_L("Nothing to commit — add a feature first"));
@@ -10990,10 +11190,9 @@ void DesignPanel::update_rib_gizmo()
m_rib_thickness ? m_rib_thickness->GetValue() : 0.0);
}
-// Onshape default planes: the XY/XZ/YZ reference planes are persistent, transparent, labelled, and
-// larger than the bed — shown as the FALLBACK when there is no object yet. When the Plane tool is
-// open they additionally surface existing datums so a base can be picked. Single authority for the
-// reference-plane overlay (set/clear_base_pick).
+// The XY/XZ/YZ reference planes and their axes. Single authority for the overlay
+// (set/clear_base_pick): the Plane tool offers them with the datums as Offset bases; otherwise they
+// are up while the Feature tree's Origin row shows them, and while a sketch plane is being chosen.
void DesignPanel::update_reference_planes()
{
if (!m_viewport) return;
@@ -11020,26 +11219,117 @@ void DesignPanel::update_reference_planes()
}
return;
}
- // Fallback (Onshape default planes): show the 3 reference planes while there is no SOLID body
- // yet — so they persist through the 2D-sketch phase and reappear after a sketch is confirmed
- // (a sketch creates no body). They no longer block selection: clicking existing geometry wins,
- // a base-plane pick only fires on a click that hit nothing else (see on_mouse fall-through).
- // Available while there is no solid yet OR while the user is actually choosing a sketch
- // plane. The second half fixes a dead end: delete a sketch on a document that still has a
- // body, press Sketch, and act_sketch says "click a face or a reference plane" — with the
- // reference planes already taken away, because a body existed. The instruction was
- // impossible to follow and there was no way to start a sketch at all short of finding a
- // face to click.
- //
- // Not simply always-on: m_dbp_active both RENDERS and picks, so three translucent planes
- // would otherwise float over every finished model. Tying them to Sketch mode shows them
- // exactly when they are the thing being chosen, and hides them again on Finish.
- if (m_doc.bodies.empty() || m_ui_mode == UiMode::Sketch)
+ // Up while Sketch waits for a plane to be picked (start_sketch), which the session it opens
+ // takes away; a live session would draw none anyway (DesignSketchTool::draws_reference_axes).
+ // They never block selection: clicking existing geometry wins, and a base-plane pick only
+ // fires on a click that hit nothing else (see on_mouse fall-through).
+ if (m_show_origin || m_choosing_sketch_plane)
m_viewport->set_base_pick(std::move(bp), std::move(bi), std::move(bl));
else
m_viewport->clear_base_pick();
}
+void DesignPanel::toggle_origin()
+{
+ m_show_origin = !m_show_origin;
+ wxGetApp().app_config->set_bool("design_show_origin", m_show_origin);
+ // A plane picked while they were up is not a selection once nobody can see it.
+ if (!m_show_origin) drop_plane_pick();
+ // Not an edit: a plane choice under way stays, and keeps its planes up.
+ update_reference_planes(); // repaints
+ refresh_pinned(); // the Origin row's eye
+}
+
+void DesignPanel::toggle_bed()
+{
+ m_show_bed = !m_show_bed;
+ wxGetApp().app_config->set_bool("design_show_bed", m_show_bed);
+ if (m_viewport) m_viewport->set_show_bed(m_show_bed); // repaints
+ refresh_pinned(); // the Bed row's eye
+}
+
+// View switches, not history: a view toggle rebuilds these two rows and leaves the features
+// alone. The label dims while its thing is hidden, as a hidden body's does.
+void DesignPanel::refresh_pinned()
+{
+ std::vector rows;
+ auto add = [&rows](const char* icon, const wxString& label, bool shown, const wxString& tip) {
+ DesignRowList::Action eye = eye_action(shown);
+ eye.tip = tip;
+ // A blank cell under the features' Delete, so the eye sits in the features' eye column.
+ rows.push_back({ icon, label, {}, shown ? dp_item_text() : dp_item_dim(), { eye, {} } });
+ };
+ // In kOriginRow / kBedRow order.
+ add("design_plane", _L("Origin"), m_show_origin, origin_toggle_text(m_show_origin));
+ add("design_bed", _L("Bed"), m_show_bed, bed_toggle_text(m_show_bed));
+ m_pinned->set_rows(std::move(rows));
+}
+
+void DesignPanel::start_sketch()
+{
+ if (m_ui_mode != UiMode::Feature) return; // already sketching or constraining
+ if (m_active != Tool::None || m_text_dlg != nullptr || body_move_pending()) {
+ set_status(StatusKind::Info, _L("Finish or cancel what is open first"));
+ return;
+ }
+ wxString where;
+ if (sketch_plane_target(where)) {
+ start_sketch_on_target(true);
+ return;
+ }
+ // Nothing to sketch on yet: offer the planes and stay in Feature mode until one is picked
+ // (set_on_datum_base_picked, set_on_solid_selection_changed).
+ m_choosing_sketch_plane = true;
+ update_reference_planes();
+ update_action_bar(); // ✗ leaves the choice
+ set_status(StatusKind::Info, sketch_plane_prompt());
+}
+
+void DesignPanel::start_sketch_on_target(bool offer_tools)
+{
+ if (!m_viewport) return;
+ wxString on;
+ const SketchPlane plane = sketch_plane_from_selection(on);
+ m_viewport->begin_sketch(plane, DesignSketchTool::Mode::Select);
+ // The face or plane has been CONSUMED as the sketch plane, so drop the pick. A face left live
+ // meant the next Extrude saw a selected face and push/pulled it instead of extruding the
+ // sketch just drawn — the same trap the imported-art path already guards against — and a
+ // plane left picked would open the next sketch on it, unseen once the planes are hidden.
+ // The canvas's half goes too: left lit there, the next click on that face read as a second
+ // click on it and took the whole body, which has no plane to sketch on.
+ drop_solid_pick();
+ m_plane_picked = false;
+ m_construction->SetValue(false); // a fresh session starts non-construction
+ set_ui_mode(UiMode::Sketch); // ends the plane choice, so the planes go
+ m_sketch_on = on; // shown with the tool hint, so the target is visible
+ set_status(StatusKind::Info, wxString::Format(_L("Sketch plane: %s — pick a tool"), on));
+ if (m_sketch_hint) { // the card must agree with the status line, not argue with it
+ m_sketch_hint->SetLabel(wxString::Format(_L("Sketch plane: %s.\nPick a tool, or press Menu for the list."), on));
+ m_sketch_hint->Refresh();
+ m_cards->Layout();
+ }
+ // Hand over the tools rather than naming them in a status line: the sketch bar carries none.
+ // CallAfter so the mode change has settled, and the pick that got here has returned, before a
+ // modal menu takes the loop; the menu carries each tool's shortcut, so pressing the key
+ // instead of picking a row costs nothing.
+ if (offer_tools)
+ CallAfter([this] { show_offer_menu(offer_anchor()); });
+}
+
+void DesignPanel::end_sketch_plane_choice()
+{
+ if (!m_choosing_sketch_plane) return;
+ m_choosing_sketch_plane = false;
+ update_reference_planes();
+ update_action_bar();
+}
+
+bool DesignPanel::sketch_plane_pick_live() const
+{
+ return m_choosing_sketch_plane && m_ui_mode == UiMode::Feature && m_active == Tool::None
+ && m_text_dlg == nullptr && !body_move_pending();
+}
+
TriangleMesh DesignPanel::ghost_from_bodies(const std::vector& per_body) const
{
TriangleMesh out;
@@ -11197,10 +11487,12 @@ void DesignPanel::refresh_preview()
// Confirm so the user sees the gate before clicking; the red status says why.
m_candidate_ok = ok;
update_confirm_button();
- // Fillet/Chamfer/Draft: once the target edge/face yields a valid result, show ONLY the
+ // Fillet/Chamfer/Draft/Hole: once the target edge/face yields a valid result, show ONLY the
// preview (hide the base bodies) so the user sees the finished shape, not the old solid
- // doubled with the ghost. Before a valid pick the body stays visible so it can be picked.
- m_viewport->set_body_hidden((m_active == Tool::Dressup || m_active == Tool::Draft) && ok);
+ // doubled with the ghost. A hole's cut lies inside the old solid, so without this the body
+ // would hide it entirely. Before a valid pick the body stays visible so it can be picked.
+ m_viewport->set_body_hidden((m_active == Tool::Dressup || m_active == Tool::Draft ||
+ m_active == Tool::Hole) && ok);
m_status->Refresh();
// Refresh the in-canvas Extrude depth arrow (self-gates: only while the Extrude card is open).
@@ -11292,7 +11584,9 @@ void DesignPanel::push_polygon_params()
void DesignPanel::open_tool(Tool t)
{
+ end_body_move(true); // first: arm_transform_gizmo below takes over the same gizmo
m_active = t;
+ end_sketch_plane_choice(); // after m_active: the Plane card keeps its planes
m_candidate_ok = true; // a fresh card is confirmable until its preview says otherwise
// Fillet/Chamfer/Draft no longer fade the body see-through; instead, once a valid target
// is picked, refresh_preview hides the base bodies entirely (preview-only). Keep it opaque
@@ -11590,7 +11884,6 @@ void DesignPanel::close_tool()
if (m_viewport) m_viewport->clear_move_gizmo();
feed_bodies();
m_xf_gizmo_body = -1;
- m_move_body = -1;
}
if (m_viewport) { m_viewport->set_body_translucent(false); m_viewport->set_body_hidden(false); m_viewport->set_xray_focus(-1); } // restore the opaque solid
wxSizer* s = m_cards->GetSizer();
@@ -11639,7 +11932,7 @@ void DesignPanel::close_tool()
m_viewport->clear_datum_gizmo();
m_viewport->set_operand_bodies(-1, -1);
m_viewport->set_highlight_sketches({});
- update_reference_planes(); // back to no-tool: show the origin planes if there is no object yet
+ update_reference_planes(); // back to no-tool
update_cards_frame(); m_form->Layout();
m_form->FitInside();
update_action_bar(); // no feature tool active -> hide the bar (unless a mode keeps it)
@@ -11714,11 +12007,8 @@ void DesignPanel::cancel_tool()
void DesignPanel::tool_confirm()
{
if (m_value_cont) { confirm_value(); return; } // value card owns ribbon ✓ while a value is pending
- if (m_active == Tool::None && m_viewport && m_viewport->moving_body()) { // keep the placement, drop the gizmo
- m_viewport->clear_move_gizmo();
- m_move_body = -1;
- show_move_card(false);
- update_action_bar();
+ if (body_move_pending()) { // keep the placement, drop the gizmo
+ end_body_move(true);
set_status_ok();
return;
}
@@ -11746,20 +12036,18 @@ void DesignPanel::tool_confirm()
void DesignPanel::tool_cancel()
{
if (m_value_cont) { cancel_value(); return; } // value card owns ribbon ✗ while a value is pending
- if (m_active == Tool::None && m_viewport && m_viewport->moving_body()) { // revert to the pose at move-start
- sync_body_xform();
- if (m_move_body >= 0 && m_move_body < int(m_body_xform.size()))
- m_body_xform[m_move_body] = m_move_prev;
- m_viewport->clear_move_gizmo();
- m_move_body = -1;
- show_move_card(false);
- feed_bodies(); // re-render the reverted placement
- update_action_bar();
+ if (body_move_pending()) { // revert to the pose at move-start
+ end_body_move(false);
set_status(StatusKind::Info, _L("Move canceled"));
return;
}
if (m_active == Tool::Insert) { cancel_insert(); return; }
if (m_active != Tool::None) { cancel_tool(); return; }
+ if (m_choosing_sketch_plane) { // Sketch was waiting for a plane: no sketch, planes away
+ end_sketch_plane_choice();
+ set_status(StatusKind::Info, wxString());
+ return;
+ }
if (m_ui_mode == UiMode::Sketch) {
// THE explicit discard. Esc no longer arrives here at all (it routes through escape(),
// which cannot destroy anything), so this button is now the only way a drawn sketch is
@@ -11795,8 +12083,7 @@ bool DesignPanel::confirm_enabled() const
{
if (m_value_cont) return true;
if (m_active == Tool::None)
- return (m_viewport && m_viewport->moving_body())
- || m_ui_mode == UiMode::Sketch || m_ui_mode == UiMode::Constrain;
+ return body_move_pending() || m_ui_mode == UiMode::Sketch || m_ui_mode == UiMode::Constrain;
if (m_active == Tool::Insert) return true;
return m_candidate_ok;
}
@@ -11820,12 +12107,12 @@ CadLevel DesignPanel::escape_level() const
|| (m_viewport && m_viewport->inline_busy());
// An uncommitted delta: clicks are down on an entity that does not exist yet, or a body is
// being moved by a gizmo that has not been confirmed.
- st.gesture_active = m_viewport
- && (m_viewport->drawing_in_progress()
- || (m_active == Tool::None && m_viewport->moving_body()));
- // Something is armed and waiting for input: a feature card, a sketch draw tool, Constrain.
- // A sketch SESSION is deliberately not in this list — see escape().
+ st.gesture_active = body_move_pending()
+ || (m_viewport && m_viewport->drawing_in_progress());
+ // Something is armed and waiting for input: a feature card, Sketch waiting for its plane, a
+ // sketch draw tool, Constrain. A sketch SESSION is deliberately not in this list — see escape().
st.tool_armed = (m_active != Tool::None)
+ || m_choosing_sketch_plane
|| m_ui_mode == UiMode::Constrain
|| (m_ui_mode == UiMode::Sketch && m_viewport && !m_viewport->sketch_is_selecting());
st.has_selection = m_viewport && m_viewport->has_any_selection();
@@ -11862,7 +12149,10 @@ void DesignPanel::escape()
// CANDIDATE (never a committed feature — in edit mode reset_edit_state only forgets which
// feature was being edited, the feature itself is untouched); an armed sketch tool falls
// back to Select, leaving every entity already drawn exactly where it is.
- if (m_active != Tool::None || m_ui_mode == UiMode::Constrain) { tool_cancel(); return; }
+ if (m_active != Tool::None || m_choosing_sketch_plane || m_ui_mode == UiMode::Constrain) {
+ tool_cancel();
+ return;
+ }
if (m_viewport && m_viewport->sketch_disarm_tool()) {
set_status(StatusKind::Info, _L("Select"));
}
@@ -11873,7 +12163,8 @@ void DesignPanel::escape()
// both of which say which one they are, and never through a key pressed on the way out of
// something else. The Feature tree and Bodies rows count as selections too.
const bool row = deselect_rows();
- if ((m_viewport && m_viewport->clear_any_selection()) || row) {
+ const bool plane = drop_plane_pick();
+ if ((m_viewport && m_viewport->clear_any_selection()) || row || plane) {
set_status(StatusKind::Info, wxString());
return;
}
@@ -11896,7 +12187,7 @@ bool DesignPanel::menu_can_undo_redo(bool redo) const
{
if (m_ui_mode == UiMode::Sketch && m_viewport && m_viewport->is_sketching())
return redo ? m_viewport->can_redo_sketch_entity() : m_viewport->can_undo_sketch_entity();
- if (m_ui_mode != UiMode::Feature || m_active != Tool::None) return false;
+ if (m_ui_mode != UiMode::Feature || m_active != Tool::None || body_move_pending()) return false;
return redo ? m_doc.can_redo() : m_doc.can_undo();
}
@@ -11916,9 +12207,10 @@ void DesignPanel::update_action_bar()
wxSizer* s = m_toolbar->GetSizer();
if (s == nullptr) return;
const bool active = (m_active != Tool::None)
+ || m_choosing_sketch_plane // ✗ only: ✓ has nothing to confirm
|| m_ui_mode == UiMode::Sketch
|| m_ui_mode == UiMode::Constrain
- || (m_viewport && m_viewport->moving_body());
+ || body_move_pending();
s->Show(m_tb_action, active, true);
// HIDING THE BAR ORPHANS THE KEYBOARD, and that is the "app does not consent to sketch"
// report. The ✓/✗ live in this bar, so the click that confirms a feature leaves focus on a
@@ -11954,7 +12246,8 @@ void DesignPanel::do_undo_redo(bool redo)
// v1: act only in Feature mode. While authoring/constraining a sketch (m_ui_mode) or
// with a feature dialog open (m_active), Esc/Cancel is the way out — popping committed
// history mid-tool would be ambiguous (and could orphan the tool's referenced feature).
- if (m_ui_mode != UiMode::Feature || m_active != Tool::None) {
+ // A Move is not in the history at all, so an undo under it would keep it without asking.
+ if (m_ui_mode != UiMode::Feature || m_active != Tool::None || body_move_pending()) {
set_status(StatusKind::Info, _L("Finish or cancel the current tool first (Esc)"));
return;
}
diff --git a/src/slic3r/GUI/CAD/DesignPanel.hpp b/src/slic3r/GUI/CAD/DesignPanel.hpp
index 5e39362fad..823282bb57 100644
--- a/src/slic3r/GUI/CAD/DesignPanel.hpp
+++ b/src/slic3r/GUI/CAD/DesignPanel.hpp
@@ -383,7 +383,16 @@ private:
void update_rib_gizmo(); // in-plane slab footprint + thickness handles (Rib card)
void refresh_datum_planes(); // push resolved datum frames + per-plane u/v extents to viewport
void refresh_mate_connectors(); // push connector frames so verse + polarity are visible
- void update_reference_planes(); // persistent XY/XZ/YZ reference planes (fallback when no object)
+ void update_reference_planes(); // the XY/XZ/YZ planes + axes: Origin row, plane choice, Plane tool
+ void toggle_origin(); // the Origin row's eye (and Ctrl+Shift+O): keep the reference planes up
+ void toggle_bed(); // the Bed row's eye (and Ctrl+Shift+B): draw the printer bed, or not
+ void refresh_pinned(); // rebuild the Origin and Bed rows (their eyes and label colours)
+ // Sketch: open a session on the picked face or plane, or with none picked, put the planes up
+ // and wait for one (m_choosing_sketch_plane).
+ void start_sketch();
+ void start_sketch_on_target(bool offer_tools); // a session on sketch_plane_from_selection()
+ void end_sketch_plane_choice(); // leave the plane choice without a sketch; no-op outside it
+ bool sketch_plane_pick_live() const; // may a pick made now still open the sketch?
CadDocument m_doc;
@@ -413,7 +422,6 @@ private:
ScalableButton* m_commit_btn{nullptr}; // main face: runs the current commit mode
DropDown* m_commit_drop{nullptr}; // commit-mode choices, owned via m_flyout_keepalive
wxSizer* m_tb_doc{nullptr}; // toolbar document/view actions (new, commit, export, section, place)
- CheckBox* m_show_bed{nullptr}; // view option: draw the printer bed + plate grid, or not
wxSizer* m_box_move{nullptr}; // Move/Rotate numeric options (distance, axis, angle)
wxSizer* m_box_sketch{nullptr};
wxSizer* m_box_extrude{nullptr};
@@ -571,8 +579,20 @@ private:
// deliberately no dropdown for it. e1p.
int m_ref_plane{0};
// m_ref_plane is always a VALID plane, so it cannot itself distinguish "the user chose XY"
- // from "nobody has chosen anything yet". This does.
+ // from "nobody has chosen anything yet". This does. A selection like a picked face: the sketch
+ // opened on it uses it up, and Esc or a click on nothing lets go of it.
bool m_plane_picked{false};
+ // Sketch was pressed with nothing to sketch on: the reference planes are up and the next
+ // reference plane or flat face clicked opens the sketch on it. Still Feature mode — sketch
+ // mode is entered only with the session (start_sketch_on_target).
+ bool m_choosing_sketch_plane{false};
+ // The Feature tree's Origin row: keeps the reference planes and their axes up
+ // (update_reference_planes). A view preference (AppConfig "design_show_origin"), not part of
+ // the recipe.
+ bool m_show_origin{false};
+ // The Feature tree's Bed row: draws the printer bed and its plate grid. A view preference
+ // (AppConfig "design_show_bed"), not part of the recipe.
+ bool m_show_bed{true};
ComboBox* m_shape{nullptr};
ComboBox* m_mode{nullptr};
wxSpinCtrlDouble* m_width{nullptr};
@@ -913,12 +933,16 @@ private:
std::function m_value_cont; // deferred apply, run on Confirm
std::function m_value_cancel; // optional action when the card is cancelled
- // Feature tree: one row per feature, in feature order, with a per-type icon and the row's
- // own Edit / Show-hide / Delete icons. Callers use row indices via
- // tree_selection()/set_tree_selection(); refresh_tree() rebuilds the rows.
+ // Feature tree: the Origin and Bed rows, a fixed block of view switches that never scrolls
+ // and selects nothing, then, in a frame of its own below them, one row per feature, in feature
+ // order, with a per-type icon and the row's own Zoom to selection / Edit / Show-hide / Delete
+ // icons. Callers use row indices via tree_selection()/set_tree_selection(); refresh_tree()
+ // rebuilds the rows.
+ DesignRowList* m_pinned{nullptr};
DesignRowList* m_tree{nullptr};
- // The faces the selected feature row made (CadDocument::faces_made_by), drawn as selected.
- // Finding them replays the history, so they are kept per row and topology generation.
+ // The faces the selected feature row made (CadDocument::faces_made_by), drawn as selected, and
+ // what Zoom to selection frames for a row. Finding them replays the history, so they are kept
+ // per row and topology generation (feature_faces).
// request_feature_highlight() refreshes them after the current event; every change of row,
// card or topology calls it.
int m_hl_feature{-1}; // the row m_hl_faces were found for...
@@ -927,7 +951,10 @@ private:
bool m_hl_pending{false};
void request_feature_highlight();
void update_feature_highlight();
+ // The faces feature `f` made, found again when the cached ones are another row's or topology's.
+ const std::vector>& feature_faces(int f);
bool deselect_rows(); // Esc / a click on nothing: drop the tree and Bodies rows
+ bool drop_plane_pick(); // ...and a picked reference plane; true if one was picked
// Bodies list under the feature tree: one row per body (parallel to m_doc.bodies). Selecting
// one highlights that body and makes it the target for the next op.
DesignRowList* m_parts{nullptr};
@@ -964,8 +991,17 @@ private:
void rebuild_disp_meshes(); // recompute m_disp_* from m_doc + m_body_xform
void feed_bodies(); // push m_disp_* + visibility/xform to the viewport
void on_move_body(); // start the move gizmo on the selected body
+ bool body_move_pending() const { return m_move_body >= 0; } // the Move button's session is open
+ void end_body_move(bool keep); // leave it: keep the dragged pose, or put the body back
void arm_transform_gizmo(); // arm the move gizmo on the Transform card's body (add mode only)
void on_set_body_color(); // Color tool: pick a per-body display colour override
+ // Zoom to selection: frame one thing along the current view, as the canvas's Fit button frames
+ // a selection. A body is framed whole, hidden or not; a feature by its sketch or the faces it
+ // made; anything else the offer is opened on, by what the Fit button frames for it.
+ void zoom_to_body(int b);
+ void zoom_to_feature(int f);
+ void zoom_to_selection();
+ bool can_zoom_to_feature(int f) const; // a sketch, or a shown feature that makes faces
void on_boolean_tool(); // Boolean (combine bodies): needs two solids, then opens the tool
int tree_selection() const; // selected feature row, or wxNOT_FOUND
int tree_body_selection() const; // selected Parts-list body index, or -1
diff --git a/src/slic3r/GUI/CAD/DesignRowList.cpp b/src/slic3r/GUI/CAD/DesignRowList.cpp
index facf0aa5ed..1c39009cd5 100644
--- a/src/slic3r/GUI/CAD/DesignRowList.cpp
+++ b/src/slic3r/GUI/CAD/DesignRowList.cpp
@@ -29,11 +29,19 @@ static constexpr int kCellDip = 20; // square action cell, hover chip included
static constexpr int kPadDip = 4; // row edges, and the gap before the action cells
static constexpr int kGapDip = 6; // type icon to label
-DesignRowList::DesignRowList(wxWindow* parent, int max_visible)
- // wxVListBox defaults to wxBORDER_THEME; the sidebar's lists take a simple frame.
- : wxVListBox(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_SIMPLE)
+DesignRowList::DesignRowList(wxWindow* parent, int max_visible, bool selectable, long style)
+ // wxVListBox defaults to wxBORDER_THEME; `style` defaults to the simple frame the sidebar's
+ // lists take, and a list meant to sit unframed on its card passes wxBORDER_NONE.
+ : wxVListBox(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, style)
, m_max_visible(std::max(max_visible, 1))
+ , m_selectable(selectable)
{
+ // A non-selectable list takes no focus: from the mouse because it consumes the presses that
+ // would focus it, below, and from the keyboard through AcceptsFocus(). wxGTK reads that once,
+ // while the base class is being built, before the override exists, so it is told here.
+ if (!m_selectable)
+ SetCanFocus(false);
+
Bind(wxEVT_LISTBOX, [this](wxCommandEvent&) { if (on_select) on_select(); });
Bind(wxEVT_LISTBOX_DCLICK, [this](wxCommandEvent&) { if (on_activate) on_activate(); });
@@ -46,18 +54,20 @@ DesignRowList::DesignRowList(wxWindow* parent, int max_visible)
m_pressed = Hit{};
e.Skip();
});
- // A press on an action cell is remembered and skipped, so the list still selects the row.
+ // A press on an action cell is remembered and skipped, so the list still selects the row. A
+ // non-selectable list keeps every press from wxVListBox, which would select the row and take
+ // the focus.
Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent& e) {
const Hit h = hit_test(e.GetPosition());
m_pressed = h.cell >= 0 ? h : Hit{};
- e.Skip();
+ if (m_selectable) e.Skip();
});
// The second press of a double-click on a cell is a press too, and is not skipped: the list
// would otherwise turn it into a row double-click, so a quick double toggle of the eye would
// also open the feature for editing.
Bind(wxEVT_LEFT_DCLICK, [this](wxMouseEvent& e) {
const Hit h = hit_test(e.GetPosition());
- if (h.cell < 0) { m_pressed = Hit{}; e.Skip(); return; }
+ if (h.cell < 0) { m_pressed = Hit{}; if (m_selectable) e.Skip(); return; }
m_pressed = h;
});
Bind(wxEVT_LEFT_UP, [this](wxMouseEvent& e) {
@@ -69,9 +79,9 @@ DesignRowList::DesignRowList(wxWindow* parent, int max_visible)
const int id = m_rows[pressed.row].actions[pressed.cell].id;
// After the click has finished dispatching: the action may rebuild these rows. The row
// must still exist and still be the selected one, or the click is dropped rather than
- // applied to whatever row took its place.
+ // applied to whatever row took its place. A non-selectable list has no selected row.
CallAfter([this, row = pressed.row, id] {
- if (row < int(GetItemCount()) && row == GetSelection() && on_action)
+ if (row < int(GetItemCount()) && (!m_selectable || row == GetSelection()) && on_action)
on_action(row, id);
});
});
@@ -79,7 +89,7 @@ DesignRowList::DesignRowList(wxWindow* parent, int max_visible)
// press suppresses the wxEVT_CONTEXT_MENU that follows it.
Bind(wxEVT_RIGHT_DOWN, [this](wxMouseEvent& e) {
const Hit h = hit_test(e.GetPosition());
- if (h.row != wxNOT_FOUND) select(h.row);
+ if (m_selectable && h.row != wxNOT_FOUND) select(h.row);
e.Skip();
});
Bind(wxEVT_CONTEXT_MENU, [this](wxContextMenuEvent& e) {
@@ -95,7 +105,7 @@ DesignRowList::DesignRowList(wxWindow* parent, int max_visible)
return;
// On MSW the menu comes with the button's release, and the pointer may have moved to
// another row since the press selected one: the menu is for the row it opens over.
- select(row);
+ if (m_selectable) select(row);
if (on_menu) on_menu(row, screen);
});
// wxVListBox takes every wheel event, even with nothing to scroll, so a short list would stop
@@ -254,7 +264,7 @@ void DesignRowList::OnDrawBackground(wxDC& dc, const wxRect& rect, size_t n) con
wxColour bg;
if (IsSelected(n))
bg = StateColor::darkModeColorFor(wxColour("#BFE1DE"));
- else if (int(n) == m_hover.row)
+ else if (m_selectable && int(n) == m_hover.row) // a non-selectable row lights only its icons
bg = StateColor::darkModeColorFor(wxColour("#E5F0EE"));
if (!bg.IsOk())
return; // the list already cleared to its background colour
@@ -314,7 +324,7 @@ DesignRowList::Hit DesignRowList::hit_test(const wxPoint& pt) const
const wxRect rect = GetItemRect(h.row);
const auto& acts = m_rows[h.row].actions;
for (size_t i = 0; i < acts.size(); ++i)
- if (cell_rect(rect, acts.size(), i).Contains(pt)) {
+ if (!acts[i].icon.empty() && cell_rect(rect, acts.size(), i).Contains(pt)) {
h.cell = int(i);
break;
}
diff --git a/src/slic3r/GUI/CAD/DesignRowList.hpp b/src/slic3r/GUI/CAD/DesignRowList.hpp
index ff1ecc482a..7c2c524691 100644
--- a/src/slic3r/GUI/CAD/DesignRowList.hpp
+++ b/src/slic3r/GUI/CAD/DesignRowList.hpp
@@ -29,12 +29,16 @@ namespace Slic3r { namespace GUI {
// Selection follows wxTreeCtrl's contract, which DesignPanel's mutual exclusion between the two
// lists relies on: on_select runs on every change, whether the user or select() made it, and
// selecting the row that is already selected changes nothing and notifies nobody.
+//
+// A list built non-selectable is a row of switches, not of objects: a click never selects a row,
+// the pointer highlights only the action icon under it, and the list never takes the focus. Its
+// action icons and its context menu are the only things that answer.
class DesignRowList : public wxVListBox
{
public:
struct Action {
int id; // the owner's code for it, handed back to on_action
- std::string icon; // icon name, e.g. "design_eye"
+ std::string icon; // icon name, e.g. "design_eye"; empty for a blank cell that only holds a column
wxString tip;
};
struct Row {
@@ -50,7 +54,8 @@ public:
static constexpr const char* hover_chip = "#D4D4D4";
// The list is as tall as its rows, at least one and at most `max_visible`; past that it scrolls.
- DesignRowList(wxWindow* parent, int max_visible);
+ // `style` is its frame: wxBORDER_NONE for a list that sits unframed on its card.
+ DesignRowList(wxWindow* parent, int max_visible, bool selectable = true, long style = wxBORDER_SIMPLE);
// Replace every row. Clears the selection without notifying and cancels a rename in progress;
// the owner re-selects the row it keeps, which notifies.
@@ -67,15 +72,20 @@ public:
std::function on_select; // the selection changed
std::function on_activate; // the selected row was double-clicked
- // A row's action icon was clicked. Runs after the click has finished dispatching, and only
- // while that row is still the selected one; the click itself selected it.
+ // A row's action icon was clicked. Runs after the click has finished dispatching, while the
+ // row still exists and — in a selectable list, where the click itself selected it — is still
+ // the selected one.
std::function on_action;
- // Context menu on a row, at a screen position; the row under the pointer is selected first.
+ // Context menu on a row, at a screen position; the row under the pointer is selected first,
+ // unless the list is non-selectable. Pop the menu from inside the call: wxGTK sends it on the
+ // right press, and a menu deferred past that event closes on the button's release.
std::function on_menu;
// The editor committed `name` for `row`, trimmed and never empty (an empty commit cancels).
// Runs after the editor's own events have finished, so the owner may rebuild the rows here.
std::function on_rename;
+ bool AcceptsFocus() const override { return m_selectable && wxVListBox::AcceptsFocus(); }
+
protected:
void OnDrawItem(wxDC& dc, const wxRect& rect, size_t n) const override;
void OnDrawBackground(wxDC& dc, const wxRect& rect, size_t n) const override;
@@ -103,6 +113,7 @@ private:
std::vector m_rows;
std::map m_icons;
int m_max_visible;
+ bool m_selectable;
int m_row_h{0}; // every row's height, from measure_row()
Hit m_hover;
Hit m_pressed; // action cell under the last left press
diff --git a/src/slic3r/GUI/CAD/DesignSketchTool.cpp b/src/slic3r/GUI/CAD/DesignSketchTool.cpp
index 4d6851b473..d5007239c1 100644
--- a/src/slic3r/GUI/CAD/DesignSketchTool.cpp
+++ b/src/slic3r/GUI/CAD/DesignSketchTool.cpp
@@ -30,7 +30,7 @@
#include