Merge upstream main: cyclic print ordering, brim overlap fix, perimeter and support fixes

This commit is contained in:
Clifford Garwood
2026-07-31 01:42:48 -04:00
131 changed files with 54916 additions and 27918 deletions
+29 -4
View File
@@ -30,6 +30,7 @@ ctest --test-dir ./tests/fff_print
- C++17, selective C++20. PascalCase classes, snake_case functions/variables
- `#pragma once` for headers. Smart pointers and RAII preferred
- Parallelization via TBB — be mindful of shared state
- Always use `SetSizerAndFit(sizer)` instead of `SetSizer(sizer)` on top level window. Unless `SetSizer` must be called before the full layout is built, call `sizer->SetSizeHints(window)` afterwards in this case.
## Key Entry Points
@@ -59,7 +60,31 @@ ctest --test-dir ./tests/fff_print
## Localization & translations
- Translation catalogs live in `localization/i18n/<lang>/OrcaSlicer_<lang>.po`.
- When creating or reviewing translations, use the [Localization glossary](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/guides/localization_glossary.md) as the source of truth for recurring terms, so the same English term is always rendered the same way within a language and terms that must stay in English (brand/product names, acronyms, file formats, G-code, macros/variables) are not translated.
- If a term's established translation changes, update both the affected `.po` files and the glossary so they stay in sync.
- Only edit `msgstr` (never `msgid`); keep placeholders (`%s`, `%1%`, `\n`), context (`msgctxt`), and file encoding/line endings intact.
Catalogs live in `localization/i18n/<lang>/OrcaSlicer_<lang>.po`; the template is `OrcaSlicer.pot`.
See the [Localization guide](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/guides/localization_guide.md) for the human-facing version of these principles.
### Terminology
- Use the [Localization glossary](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/main/guides/localization_glossary.md) as the source of truth for recurring terms, so the same English term is always rendered the same way within a language, and terms that must stay in English (brand/product names, acronyms, materials, file formats, G-code tokens, macros/variables/identifiers) are not translated.
- If a term's established translation changes, update both the affected `.po` files and the glossary (`localization_glossary.tsv`, then regenerate) so they stay in sync.
- Translate the *meaning*, not the words. Check what the string actually controls before translating it — English reuses one word for different things. `Flow ratio` (multiplier), `Flow Rate` (throughput) and `Flow Dynamics` (pressure compensation) are three different terms; `extruder` may mean the toolhead, the feeder motor, or the nozzle depending on the string.
- Reuse one template per recurring message shape (`Failed to connect to …`, `Are you sure you want to …?`), even where the English wording varies.
### Editing rules
- Only edit `msgstr` — **never** change `msgid`, and never "fix" wrong English in the translation alone. Report the source string instead.
- Preserve exactly: placeholders (`%s`, `%d`, `%1%`, `%zu`, `%%`), every `\n` (count *and* position, including leading/trailing), leading/trailing spaces, HTML tags, `℃`, and the file's encoding and line endings.
- **Never reorder positional arguments** in a `c-format` string. If the msgid is `%d` then `%s`, that order must hold — swapping them breaks at runtime.
- `msgctxt` separates homonyms — always read it. `Back`/`Camera View` is the rear view of the 3D navigator, while `Back`/`Navigation` is the go-back button; `Top` exists in the *Alignment*, *Layers* and *Camera View* senses.
- When a string needs disambiguating, add context in the source (`_L_CONTEXT`/`_u8L_CONTEXT`), don't work around it in the translation.
- A literal `%` inside a string xgettext flagged `possible-c-format` will fail `msgfmt`. Fix it with a `// xgettext:no-c-format, no-boost-format` comment above the string in the source — do not mangle the translation or use `%%` in text that is never passed through printf.
- Plural entries: read `nplurals` from the catalog's `Plural-Forms` header (it is **not** always 2 — ja/ko/zh/th/vi use 1, ru/cs/pl/lt use 3, uk uses 4). Each form must be genuinely inflected for its quantity; repeating one sentence across all forms is a bug in Slavic/Baltic languages, though it is correct for Turkish and Hungarian.
- An entry whose `msgstr` equals its `msgid` is untranslated even though it is not empty; a plural entry with any empty form is likewise incomplete.
- Mark machine-produced translations with an `# AI Translated` translator comment. Don't add it to a human translation you didn't actually rewrite.
- Don't reflow or re-wrap unrelated entries — keep the diff limited to the strings you changed.
### Verifying
- `scripts/run_gettext.bat --full` (Windows) regenerates the template, merges every catalog and compiles the `.mo` files. It must exit 0.
- Or check a single catalog with `msgfmt --check-format -o <out>.mo localization/i18n/<lang>/OrcaSlicer_<lang>.po`.
- Fuzzy entries are not shown to users. If you correct one, clear its `fuzzy` flag, otherwise the fix never ships.
+4
View File
@@ -119,6 +119,10 @@ Download the **Windows Installer exe** for your preferred version from the [rele
- This file may already be available on your computer if you've installed visual studio. Check the following location: `%VCINSTALLDIR%Redist\MSVC\v142`
</details>
### Microsoft Store
Install from the [Microsoft Store](https://apps.microsoft.com/detail/9mv6gl23xm59) when you prefer a Store-signed package (helps on Windows 11 Smart App Control).
### Windows Package Manager
```shell
+2
View File
@@ -423,6 +423,7 @@ endif ()
include(OCCT/OCCT.cmake)
include(OpenCV/OpenCV.cmake)
include(python3/python3.cmake)
include(wxInspector/wxInspector.cmake)
set(_dep_list
dep_Boost
@@ -446,6 +447,7 @@ set(_dep_list
${EXPAT_PKG}
dep_libnoise
dep_python3
dep_wxInspector
)
if (MSVC)
+13
View File
@@ -0,0 +1,13 @@
orcaslicer_add_cmake_project(
wxInspector
URL https://github.com/Noisyfox/wxInspector/archive/refs/tags/v1.0.0.zip
URL_HASH SHA256=0ba163956f2d468b19a91b96c5aba66ee9610843ea41dda628ea44cdafde7db7
DEPENDS ${WXWIDGETS_PKG}
CMAKE_ARGS
-DCMAKE_CXX_FLAGS="-DwxDEBUG_LEVEL=0"
-DCMAKE_POSITION_INDEPENDENT_CODE=ON
)
if (MSVC)
add_debug_dep(dep_wxInspector)
endif ()
+1
View File
@@ -26,6 +26,7 @@ orcaslicer_add_cmake_project(
GIT_REPOSITORY "https://github.com/SoftFever/Orca-deps-wxWidgets"
GIT_TAG v3.3.2
GIT_SHALLOW ON
GIT_SUBMODULES 3rdparty/catch 3rdparty/pcre 3rdparty/libwebp
DEPENDS ${PNG_PKG} ${ZLIB_PKG} ${EXPAT_PKG} ${JPEG_PKG}
CMAKE_ARGS
-DwxBUILD_PRECOMP=ON
@@ -0,0 +1,111 @@
# Move `wxInspectable` into `DPIAware` — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Move `wxInspector::wxInspectable` from individual leaf classes into the common `DPIAware<P>` template so every DPIAware widget is automatically inspectable and gets the inspector keyboard shortcut.
**Architecture:** `DPIAware<P>` gains `wxInspector::wxInspectable` as a second base class and calls `SetupInspectorAccelerator(this)` in its constructor. `DPIDialog` and `MainFrame` drop their now-redundant `wxInspectable` inheritance and `SetupInspectorAccelerator` calls.
**Tech Stack:** C++17, wxWidgets, wxInspector
## Global Constraints
- Build with `D:\VisualStudio\2026\Community\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe`
- Use `--config RelWithDebInfo` for all builds
- Cross-platform: must compile on Windows, macOS, and Linux
- Match existing code style: PascalCase classes, `#pragma once`
- Do NOT commit files under `.superpowers/`
- Do NOT commit `task.md`
---
### Task 1: Move `wxInspectable` and `SetupInspectorAccelerator` into `DPIAware<P>`
**Files:**
- Modify: `src/slic3r/GUI/GUI_Utils.hpp:92` (DPIAware template — add wxInspectable base + SetupInspectorAccelerator call)
- Modify: `src/slic3r/GUI/GUI_Utils.hpp:276` (DPIDialog — drop wxInspectable + SetupInspectorAccelerator)
- Modify: `src/slic3r/GUI/MainFrame.hpp:96` (MainFrame — drop wxInspectable)
- Modify: `src/slic3r/GUI/MainFrame.cpp:304` (MainFrame constructor — drop SetupInspectorAccelerator)
**Interfaces:**
- Consumes: Nothing (standalone refactor)
- Produces: All DPIAware widgets automatically inherit `wxInspector::wxInspectable` and get Ctrl+Shift+I accelerator
- [ ] **Step 1: Add `wxInspectable` to `DPIAware<P>` and call `SetupInspectorAccelerator`**
In `src/slic3r/GUI/GUI_Utils.hpp`, line 92, change the base class:
```cpp
// Before:
template<class P> class DPIAware : public P
// After:
template<class P> class DPIAware : public P, public wxInspector::wxInspectable
```
In the constructor body of `DPIAware<P>`, after `this->CenterOnParent();` (currently line 110), add:
```cpp
SetupInspectorAccelerator(this);
```
(`<wx/inspector/inspector.h>` is already included at line 23.)
- [ ] **Step 2: Remove redundant `wxInspectable` and `SetupInspectorAccelerator` from `DPIDialog`**
In `src/slic3r/GUI/GUI_Utils.hpp`, line 276, change:
```cpp
// Before:
class DPIDialog : public DPIAware<wxDialog>, public wxInspector::wxInspectable
// After:
class DPIDialog : public DPIAware<wxDialog>
```
In the `DPIDialog` constructor body, remove the `SetupInspectorAccelerator(this);` line (currently line 286). The rest of the constructor stays.
- [ ] **Step 3: Remove redundant `wxInspectable` from `MainFrame`**
In `src/slic3r/GUI/MainFrame.hpp`, line 96, change:
```cpp
// Before:
class MainFrame : public DPIFrame, public wxInspector::wxInspectable
// After:
class MainFrame : public DPIFrame
```
`MainFrame` now gets `wxInspectable` through `DPIFrame` → `DPIAware<wxFrame>`.
- [ ] **Step 4: Remove redundant `SetupInspectorAccelerator` from `MainFrame` constructor**
In `src/slic3r/GUI/MainFrame.cpp`, line 304, remove the line:
```cpp
SetupInspectorAccelerator(this);
```
It is now called automatically by the `DPIAware<wxFrame>` constructor.
- [ ] **Step 5: Build to verify compilation**
```powershell
$cmakePath = "D:\VisualStudio\2026\Community\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe"
& $cmakePath --build . --config RelWithDebInfo --target ALL_BUILD -- -m
```
Expected: Build succeeds with zero new errors or warnings.
- [ ] **Step 6: Commit**
```bash
git add src/slic3r/GUI/GUI_Utils.hpp src/slic3r/GUI/MainFrame.hpp src/slic3r/GUI/MainFrame.cpp
git commit -m "refactor: move wxInspectable and SetupInspectorAccelerator into DPIAware
DPIAware<P> now inherits wxInspector::wxInspectable and calls
SetupInspectorAccelerator in its constructor, making all DPIAware
widgets automatically appear in the inspector tree with the
Ctrl+Shift+I shortcut. Remove redundant wxInspectable inheritance
and SetupInspectorAccelerator calls from DPIDialog and MainFrame.
Co-Authored-By: Claude <noreply@anthropic.com>"
```
@@ -0,0 +1,753 @@
# wxInspector Plugins for OrcaSlicer — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Build two wxInspector plugins (DPIAware + CustomWidgets) that expose OrcaSlicer custom control properties in the inspector's property grid.
**Architecture:** Two plugins in a shared folder under `src/slic3r/Utils/wxInspectorPlugins/`. DPIAwarePlugin uses `dynamic_cast<DPIFrame*>/<DPIDialog*>` for detection; CustomWidgetsPlugin uses per-type `dynamic_cast`. Both registered as static singletons via a single inline function in `Registration.hpp`, called from `MainFrame` constructor.
**Tech Stack:** C++17, wxWidgets, wxInspector plugin API (`wx/inspector/plugin.h`, `wx/inspector/inspector.h`), OrcaSlicer custom widget headers
## Global Constraints
- Plugins placed under `src/slic3r/Utils/wxInspectorPlugins/`
- Build with `D:\VisualStudio\2026\Community\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe`
- Minimal source changes: only trivial (one-line) getters/setters added to existing classes
- Cross-platform: must compile on Windows, macOS, and Linux
- Match existing code style: PascalCase classes, snake_case functions, `#pragma once`
---
### Task 1: Add getters/setters to existing Orca widget headers
**Files:**
- Modify: `src/slic3r/GUI/GUI_Utils.hpp` (DPIAware template — add 4 methods)
- Modify: `src/slic3r/GUI/Widgets/Button.hpp` (add 3 getters)
- Modify: `src/slic3r/GUI/Widgets/CheckBox.hpp` (add 1 getter)
- Modify: `src/slic3r/GUI/Widgets/TextInput.hpp` (add 1 getter)
- Modify: `src/slic3r/GUI/Widgets/LabeledStaticBox.hpp` (add 4 getter declarations)
- Modify: `src/slic3r/GUI/Widgets/LabeledStaticBox.cpp` (add 4 getter implementations)
**Interfaces:**
- Consumes: Nothing (prerequisite for all other tasks)
- Produces:
- `DPIAware<P>::set_scale_factor(float)`, `DPIAware<P>::set_prev_scale_factor(float)`, `DPIAware<P>::set_em_unit(int)`, `DPIAware<P>::force_rescale() const`
- `Button::GetStyle()`, `Button::GetType()`, `Button::IsSelected()`
- `CheckBox::IsHalfChecked()`
- `TextInput::GetCornerRadius()`
- `LabeledStaticBox::GetCornerRadius()`, `LabeledStaticBox::GetBorderWidth()`, `LabeledStaticBox::GetBorderColor()`, `LabeledStaticBox::GetScale()`
- [ ] **Step 1: Add DPIAware setters/getter in GUI_Utils.hpp**
After line 184 (`float prev_scale_factor() const { return m_prev_scale_factor; }`), add:
```cpp
void set_scale_factor(float v) { m_scale_factor = v; }
void set_prev_scale_factor(float v) { m_prev_scale_factor = v; }
void set_em_unit(int v) { m_em_unit = v; }
bool force_rescale() const { return m_force_rescale; }
```
- [ ] **Step 2: Add Button getters in Button.hpp**
After line 79 (`void SetSelected(bool selected = true) { m_selected = selected; }`), add:
```cpp
ButtonStyle GetStyle() const { return m_style; }
ButtonType GetType() const { return m_type; }
bool IsSelected() const { return m_selected; }
```
- [ ] **Step 3: Add CheckBox getter in CheckBox.hpp**
After line 16 (`void SetHalfChecked(bool value = true);`), add:
```cpp
bool IsHalfChecked() const { return m_half_checked; }
```
- [ ] **Step 4: Add TextInput getter in TextInput.hpp**
After line 44 (`void SetCornerRadius(double radius);`), add:
```cpp
int GetCornerRadius() const { return static_cast<int>(radius); }
```
(Note: `radius` is inherited from `StaticBox` which has it as a protected `double` member.)
- [ ] **Step 5: Add LabeledStaticBox getter declarations in LabeledStaticBox.hpp**
After line 46 (`bool Enable(bool enable) override;`), add:
```cpp
int GetCornerRadius() const { return m_radius; }
int GetBorderWidth() const { return m_border_width; }
StateColor GetBorderColor() const { return border_color; }
float GetScale() const { return m_scale; }
```
(Note: all of `m_radius`, `m_border_width`, `border_color`, `m_scale` are protected members, accessible to inline methods.)
- [ ] **Step 6: Commit**
```bash
git add src/slic3r/GUI/GUI_Utils.hpp src/slic3r/GUI/Widgets/Button.hpp src/slic3r/GUI/Widgets/CheckBox.hpp src/slic3r/GUI/Widgets/TextInput.hpp src/slic3r/GUI/Widgets/LabeledStaticBox.hpp
git commit -m "feat: add getters/setters for wxInspector plugin access
Add minimal public accessors to DPIAware (set_scale_factor,
set_prev_scale_factor, set_em_unit, force_rescale), Button
(GetStyle, GetType, IsSelected), CheckBox (IsHalfChecked),
TextInput (GetCornerRadius), and LabeledStaticBox
(GetCornerRadius, GetBorderWidth, GetBorderColor, GetScale)."
```
---
### Task 2: Create Registration helper header
**Files:**
- Create: `src/slic3r/Utils/wxInspectorPlugins/Registration.hpp`
**Interfaces:**
- Consumes: Nothing (forward-declares plugin classes)
- Produces: `RegisterOrcaInspectorPlugins()`
- [ ] **Step 1: Create directory**
```bash
mkdir -p src/slic3r/Utils/wxInspectorPlugins
```
- [ ] **Step 2: Write Registration.hpp**
```cpp
#pragma once
namespace wxInspector {
class wxInspectorPlugin;
void RegisterPlugin(wxInspectorPlugin* plugin);
}
// Forward declare our plugins
class DPIAwarePlugin;
class CustomWidgetsPlugin;
inline void RegisterOrcaInspectorPlugins()
{
static DPIAwarePlugin dpiaware;
static CustomWidgetsPlugin customWidgets;
wxInspector::RegisterPlugin(&dpiaware);
wxInspector::RegisterPlugin(&customWidgets);
}
```
- [ ] **Step 3: Commit**
```bash
git add src/slic3r/Utils/wxInspectorPlugins/Registration.hpp
git commit -m "feat: add wxInspector plugin registration helper
Add RegisterOrcaInspectorPlugins() inline function that creates
and registers the DPIAwarePlugin and CustomWidgetsPlugin as
static instances (matching wxInspector's built-in pattern)."
```
---
### Task 3: Create DPIAwarePlugin
**Files:**
- Create: `src/slic3r/Utils/wxInspectorPlugins/DPIAwarePlugin.hpp`
- Create: `src/slic3r/Utils/wxInspectorPlugins/DPIAwarePlugin.cpp`
**Interfaces:**
- Consumes: Task 1 (DPIAware getters/setters), Task 2 (registration pattern)
- Produces: `class DPIAwarePlugin : public wxInspector::wxInspectorPlugin`
- [ ] **Step 1: Write DPIAwarePlugin.hpp**
```cpp
#pragma once
#include <wx/inspector/plugin.h>
class DPIAwarePlugin : public wxInspector::wxInspectorPlugin
{
public:
wxString GetName() const override;
bool CanProvideProperties(wxClassInfo* info) override;
wxVector<wxInspector::PropertyDef> GetProperties(
wxInspector::InspectableObject& obj) override;
};
```
- [ ] **Step 2: Write DPIAwarePlugin.cpp**
```cpp
#include "DPIAwarePlugin.hpp"
#include "slic3r/GUI/GUI_Utils.hpp" // DPIFrame, DPIDialog, DPIAware<P>
#include <wx/window.h>
namespace {
template<typename T>
void addDPIProps(T* dpi, wxVector<wxInspector::PropertyDef>& props)
{
using namespace wxInspector;
props.push_back({"Scale Factor", "DPI Scaling", PropertyType::String,
wxString::Format("%.2f", dpi->scale_factor()), false, {},
[dpi]() { return wxString::Format("%.2f", dpi->scale_factor()); },
[dpi](const wxString& v) {
double val;
if (wxSscanf(v, "%lf", &val) != 1) return false;
dpi->set_scale_factor((float) val);
return true;
}});
props.push_back({"Prev Scale Factor", "DPI Scaling", PropertyType::String,
wxString::Format("%.2f", dpi->prev_scale_factor()), false, {},
[dpi]() { return wxString::Format("%.2f", dpi->prev_scale_factor()); },
[dpi](const wxString& v) {
double val;
if (wxSscanf(v, "%lf", &val) != 1) return false;
dpi->set_prev_scale_factor((float) val);
return true;
}});
props.push_back({"EM Unit", "DPI Scaling", PropertyType::Integer,
wxString::Format("%d", dpi->em_unit()), false, {},
[dpi]() { return wxString::Format("%d", dpi->em_unit()); },
[dpi](const wxString& v) {
long val;
if (!v.ToLong(&val)) return false;
dpi->set_em_unit((int) val);
return true;
}});
props.push_back({"Normal Font", "DPI Scaling", PropertyType::ReadOnly,
dpi->normal_font().GetNativeFontInfoDesc(), true, {},
[dpi]() { return dpi->normal_font().GetNativeFontInfoDesc(); },
nullptr});
props.push_back({"Force Rescale", "DPI Scaling", PropertyType::Boolean,
dpi->force_rescale() ? "true" : "false", true, {},
[dpi]() { return dpi->force_rescale() ? "true" : "false"; },
nullptr});
}
} // anonymous namespace
wxString DPIAwarePlugin::GetName() const
{
return "OrcaDPIAware";
}
bool DPIAwarePlugin::CanProvideProperties(wxClassInfo* info)
{
return info->IsKindOf(CLASSINFO(wxWindow));
}
wxVector<wxInspector::PropertyDef> DPIAwarePlugin::GetProperties(
wxInspector::InspectableObject& obj)
{
wxVector<wxInspector::PropertyDef> props;
wxWindow* win = obj.AsWindow();
if (!win) return props;
if (auto* frame = dynamic_cast<DPIFrame*>(win)) {
addDPIProps(frame, props);
} else if (auto* dlg = dynamic_cast<DPIDialog*>(win)) {
addDPIProps(dlg, props);
}
return props;
}
```
- [ ] **Step 3: Commit**
```bash
git add src/slic3r/Utils/wxInspectorPlugins/DPIAwarePlugin.hpp src/slic3r/Utils/wxInspectorPlugins/DPIAwarePlugin.cpp
git commit -m "feat: add DPIAware wxInspector plugin
Exposes DPI scaling properties (scale_factor, prev_scale_factor,
em_unit, normal_font, force_rescale) on DPIFrame and DPIDialog
widgets. Uses dynamic_cast for detection and a template helper
to capture the correct static type for lambda accessors."
```
---
### Task 4: Create CustomWidgetsPlugin
**Files:**
- Create: `src/slic3r/Utils/wxInspectorPlugins/CustomWidgetsPlugin.hpp`
- Create: `src/slic3r/Utils/wxInspectorPlugins/CustomWidgetsPlugin.cpp`
**Interfaces:**
- Consumes: Task 1 (all widget getters), Task 2 (registration pattern)
- Produces: `class CustomWidgetsPlugin : public wxInspector::wxInspectorPlugin`
- [ ] **Step 1: Write CustomWidgetsPlugin.hpp**
```cpp
#pragma once
#include <wx/inspector/plugin.h>
class CustomWidgetsPlugin : public wxInspector::wxInspectorPlugin
{
public:
wxString GetName() const override;
bool CanProvideProperties(wxClassInfo* info) override;
wxVector<wxInspector::PropertyDef> GetProperties(
wxInspector::InspectableObject& obj) override;
private:
void addButtonProps(class Button* btn,
wxVector<wxInspector::PropertyDef>& props);
void addCheckBoxProps(class CheckBox* cb,
wxVector<wxInspector::PropertyDef>& props);
void addTextInputProps(class TextInput* ti,
wxVector<wxInspector::PropertyDef>& props);
void addSwitchButtonProps(class SwitchButton* sb,
wxVector<wxInspector::PropertyDef>& props);
void addProgressBarProps(class ProgressBar* pb,
wxVector<wxInspector::PropertyDef>& props);
void addLabelProps(class Label* lbl,
wxVector<wxInspector::PropertyDef>& props);
void addLabeledStaticBoxProps(class LabeledStaticBox* lsb,
wxVector<wxInspector::PropertyDef>& props);
};
```
- [ ] **Step 2: Write CustomWidgetsPlugin.cpp — includes and GetName/CanProvideProperties**
```cpp
#include "CustomWidgetsPlugin.hpp"
#include "slic3r/GUI/Widgets/Button.hpp"
#include "slic3r/GUI/Widgets/CheckBox.hpp"
#include "slic3r/GUI/Widgets/TextInput.hpp"
#include "slic3r/GUI/Widgets/SwitchButton.hpp"
#include "slic3r/GUI/Widgets/ProgressBar.hpp"
#include "slic3r/GUI/Widgets/Label.hpp"
#include "slic3r/GUI/Widgets/LabeledStaticBox.hpp"
#include <wx/window.h>
#include <wx/tglbtn.h>
wxString CustomWidgetsPlugin::GetName() const
{
return "OrcaCustomWidgets";
}
bool CustomWidgetsPlugin::CanProvideProperties(wxClassInfo* info)
{
return info->IsKindOf(CLASSINFO(wxWindow));
}
wxVector<wxInspector::PropertyDef> CustomWidgetsPlugin::GetProperties(
wxInspector::InspectableObject& obj)
{
wxVector<wxInspector::PropertyDef> props;
wxWindow* win = obj.AsWindow();
if (!win) return props;
if (auto* btn = dynamic_cast<Button*>(win))
addButtonProps(btn, props);
if (auto* cb = dynamic_cast<CheckBox*>(win))
addCheckBoxProps(cb, props);
if (auto* ti = dynamic_cast<TextInput*>(win))
addTextInputProps(ti, props);
if (auto* sb = dynamic_cast<SwitchButton*>(win))
addSwitchButtonProps(sb, props);
if (auto* pb = dynamic_cast<ProgressBar*>(win))
addProgressBarProps(pb, props);
if (auto* lbl = dynamic_cast<Label*>(win))
addLabelProps(lbl, props);
if (auto* lsb = dynamic_cast<LabeledStaticBox*>(win))
addLabeledStaticBoxProps(lsb, props);
return props;
}
```
- [ ] **Step 3: Write CustomWidgetsPlugin.cpp — addButtonProps**
```cpp
void CustomWidgetsPlugin::addButtonProps(Button* btn,
wxVector<wxInspector::PropertyDef>& props)
{
using namespace wxInspector;
wxVector<wxString> styleChoices;
styleChoices.push_back("Regular");
styleChoices.push_back("Confirm");
styleChoices.push_back("Alert");
styleChoices.push_back("Disabled");
auto styleToStr = [](ButtonStyle s) -> wxString {
switch (s) {
case ButtonStyle::Regular: return "Regular";
case ButtonStyle::Confirm: return "Confirm";
case ButtonStyle::Alert: return "Alert";
case ButtonStyle::Disabled: return "Disabled";
}
return "Regular";
};
props.push_back({"Button Style", "Orca Button", PropertyType::Choice,
styleToStr(btn->GetStyle()), false, styleChoices,
[btn, styleToStr]() { return styleToStr(btn->GetStyle()); },
[btn](const wxString& v) {
ButtonStyle s = ButtonStyle::Regular;
if (v == "Confirm") s = ButtonStyle::Confirm;
else if (v == "Alert") s = ButtonStyle::Alert;
else if (v == "Disabled") s = ButtonStyle::Disabled;
btn->SetStyle(s, btn->GetType());
return true;
}});
wxVector<wxString> typeChoices;
typeChoices.push_back("Compact");
typeChoices.push_back("Window");
typeChoices.push_back("Choice");
typeChoices.push_back("Parameter");
typeChoices.push_back("Icon");
typeChoices.push_back("Expanded");
auto typeToStr = [](ButtonType t) -> wxString {
switch (t) {
case ButtonType::Compact: return "Compact";
case ButtonType::Window: return "Window";
case ButtonType::Choice: return "Choice";
case ButtonType::Parameter: return "Parameter";
case ButtonType::Icon: return "Icon";
case ButtonType::Expanded: return "Expanded";
}
return "Compact";
};
props.push_back({"Button Type", "Orca Button", PropertyType::Choice,
typeToStr(btn->GetType()), false, typeChoices,
[btn, typeToStr]() { return typeToStr(btn->GetType()); },
[btn](const wxString& v) {
ButtonType t = ButtonType::Compact;
if (v == "Window") t = ButtonType::Window;
else if (v == "Choice") t = ButtonType::Choice;
else if (v == "Parameter") t = ButtonType::Parameter;
else if (v == "Icon") t = ButtonType::Icon;
else if (v == "Expanded") t = ButtonType::Expanded;
btn->SetStyle(btn->GetStyle(), t);
return true;
}});
props.push_back({"Selected", "Orca Button", PropertyType::Boolean,
btn->IsSelected() ? "true" : "false", false, {},
[btn]() { return btn->IsSelected() ? "true" : "false"; },
[btn](const wxString& v) {
btn->SetSelected(v == "true");
btn->Refresh();
return true;
}});
}
```
- [ ] **Step 4: Write CustomWidgetsPlugin.cpp — addCheckBoxProps**
```cpp
void CustomWidgetsPlugin::addCheckBoxProps(CheckBox* cb,
wxVector<wxInspector::PropertyDef>& props)
{
using namespace wxInspector;
props.push_back({"Half Checked", "Orca CheckBox", PropertyType::Boolean,
cb->IsHalfChecked() ? "true" : "false", false, {},
[cb]() { return cb->IsHalfChecked() ? "true" : "false"; },
[cb](const wxString& v) {
cb->SetHalfChecked(v == "true");
return true;
}});
}
```
- [ ] **Step 5: Write CustomWidgetsPlugin.cpp — addTextInputProps**
```cpp
void CustomWidgetsPlugin::addTextInputProps(TextInput* ti,
wxVector<wxInspector::PropertyDef>& props)
{
using namespace wxInspector;
props.push_back({"Label", "Orca TextInput", PropertyType::String,
ti->GetLabel(), false, {},
[ti]() { return ti->GetLabel(); },
[ti](const wxString& v) { ti->SetLabel(v); return true; }});
props.push_back({"Text Value", "Orca TextInput", PropertyType::String,
ti->GetTextCtrl()->GetValue(), false, {},
[ti]() { return ti->GetTextCtrl()->GetValue(); },
[ti](const wxString& v) { ti->GetTextCtrl()->SetValue(v); return true; }});
props.push_back({"Corner Radius", "Orca TextInput", PropertyType::Integer,
wxString::Format("%d", ti->GetCornerRadius()), false, {},
[ti]() { return wxString::Format("%d", ti->GetCornerRadius()); },
[ti](const wxString& v) {
long val;
if (!v.ToLong(&val)) return false;
ti->SetCornerRadius((double) val);
ti->Refresh();
return true;
}});
}
```
- [ ] **Step 6: Write CustomWidgetsPlugin.cpp — addSwitchButtonProps**
```cpp
void CustomWidgetsPlugin::addSwitchButtonProps(SwitchButton* sb,
wxVector<wxInspector::PropertyDef>& props)
{
using namespace wxInspector;
props.push_back({"Value", "Orca SwitchButton", PropertyType::Boolean,
sb->GetValue() ? "true" : "false", false, {},
[sb]() { return sb->GetValue() ? "true" : "false"; },
[sb](const wxString& v) {
sb->SetValue(v == "true");
return true;
}});
}
```
(Note: `GetValue()` and `SetValue()` are inherited from `wxBitmapToggleButton` → `wxToggleButton`.)
- [ ] **Step 7: Write CustomWidgetsPlugin.cpp — addProgressBarProps**
```cpp
void CustomWidgetsPlugin::addProgressBarProps(ProgressBar* pb,
wxVector<wxInspector::PropertyDef>& props)
{
using namespace wxInspector;
props.push_back({"Proportion", "Orca ProgressBar", PropertyType::String,
wxString::Format("%.2f", pb->m_proportion), false, {},
[pb]() { return wxString::Format("%.2f", pb->m_proportion); },
[pb](const wxString& v) {
double val;
if (wxSscanf(v, "%lf", &val) != 1) return false;
pb->m_proportion = val;
pb->Refresh();
return true;
}});
props.push_back({"Show Number", "Orca ProgressBar", PropertyType::Boolean,
pb->m_shownumber ? "true" : "false", false, {},
[pb]() { return pb->m_shownumber ? "true" : "false"; },
[pb](const wxString& v) {
pb->m_shownumber = (v == "true");
pb->Refresh();
return true;
}});
}
```
(Note: `m_proportion` and `m_shownumber` are public members on `ProgressBar`.)
- [ ] **Step 8: Write CustomWidgetsPlugin.cpp — addLabelProps**
```cpp
void CustomWidgetsPlugin::addLabelProps(Label* lbl,
wxVector<wxInspector::PropertyDef>& props)
{
using namespace wxInspector;
bool isHyperlink = (lbl->GetWindowStyleFlag() & 0x0020) != 0; // LB_HYPERLINK
props.push_back({"Is Hyperlink", "Orca Label", PropertyType::Boolean,
isHyperlink ? "true" : "false", true, {},
[lbl]() {
return (lbl->GetWindowStyleFlag() & 0x0020) ? "true" : "false";
},
nullptr});
props.push_back({"Font Point Size", "Orca Label", PropertyType::ReadOnly,
wxString::Format("%d", lbl->GetFont().GetPointSize()), true, {},
[lbl]() {
return wxString::Format("%d", lbl->GetFont().GetPointSize());
},
nullptr});
}
```
- [ ] **Step 9: Write CustomWidgetsPlugin.cpp — addLabeledStaticBoxProps**
```cpp
void CustomWidgetsPlugin::addLabeledStaticBoxProps(LabeledStaticBox* lsb,
wxVector<wxInspector::PropertyDef>& props)
{
using namespace wxInspector;
props.push_back({"Corner Radius", "LabeledStaticBox", PropertyType::Integer,
wxString::Format("%d", lsb->GetCornerRadius()), false, {},
[lsb]() { return wxString::Format("%d", lsb->GetCornerRadius()); },
[lsb](const wxString& v) {
long val;
if (!v.ToLong(&val)) return false;
lsb->SetCornerRadius((int) val);
return true;
}});
props.push_back({"Border Width", "LabeledStaticBox", PropertyType::Integer,
wxString::Format("%d", lsb->GetBorderWidth()), false, {},
[lsb]() { return wxString::Format("%d", lsb->GetBorderWidth()); },
[lsb](const wxString& v) {
long val;
if (!v.ToLong(&val)) return false;
lsb->SetBorderWidth((int) val);
return true;
}});
// Border Color: display as hex string
wxColour bc = lsb->GetBorderColor().colorForStates(0);
props.push_back({"Border Color", "LabeledStaticBox", PropertyType::String,
bc.GetAsString(wxC2S_HTML_SYNTAX), false, {},
[lsb]() {
return lsb->GetBorderColor()
.colorForStates(0)
.GetAsString(wxC2S_HTML_SYNTAX);
},
[lsb](const wxString& v) {
wxColour c(v);
if (!c.IsOk()) return false;
lsb->SetBorderColor(StateColor(c));
return true;
}});
props.push_back({"Scale", "LabeledStaticBox", PropertyType::ReadOnly,
wxString::Format("%.2f", lsb->GetScale()), true, {},
[lsb]() { return wxString::Format("%.2f", lsb->GetScale()); },
nullptr});
}
```
- [ ] **Step 10: Commit**
```bash
git add src/slic3r/Utils/wxInspectorPlugins/CustomWidgetsPlugin.hpp src/slic3r/Utils/wxInspectorPlugins/CustomWidgetsPlugin.cpp
git commit -m "feat: add OrcaCustomWidgets wxInspector plugin
Exposes Orca-specific properties on 7 widget types:
- Button: Style, Type, Selected
- CheckBox: Half Checked
- TextInput: Label, Text Value, Corner Radius
- SwitchButton: Value
- ProgressBar: Proportion, Show Number
- Label: Is Hyperlink, Font Point Size
- LabeledStaticBox: Corner Radius, Border Width, Border Color, Scale
Each widget type uses dynamic_cast for safe detection."
```
---
### Task 5: Wire plugins into MainFrame and CMakeLists
**Files:**
- Modify: `src/slic3r/GUI/MainFrame.cpp` (add include + registration call)
- Modify: `src/slic3r/CMakeLists.txt` (add 4 source files)
**Interfaces:**
- Consumes: Tasks 1-4 (all plugins and registration helper)
- Produces: Registered plugins available at runtime, buildable project
- [ ] **Step 1: Add include in MainFrame.cpp**
After the existing includes (around line 30, near the other Utils includes), add:
```cpp
#include "slic3r/Utils/wxInspectorPlugins/Registration.hpp"
```
- [ ] **Step 2: Add registration call in MainFrame constructor**
After `SetupInspectorAccelerator(this);` (currently line ~303), add:
```cpp
RegisterOrcaInspectorPlugins();
```
- [ ] **Step 3: Add source files to CMakeLists.txt**
Find the `SLIC3R_GUI_SOURCES` list in `src/slic3r/CMakeLists.txt`. After the existing `Utils/*.cpp` entries (around line 650-754), add:
```cmake
Utils/wxInspectorPlugins/DPIAwarePlugin.hpp
Utils/wxInspectorPlugins/DPIAwarePlugin.cpp
Utils/wxInspectorPlugins/CustomWidgetsPlugin.hpp
Utils/wxInspectorPlugins/CustomWidgetsPlugin.cpp
Utils/wxInspectorPlugins/Registration.hpp
```
(Note: Add all 5 files — 2 .hpp + 2 .cpp + 1 Registration.hpp. wxWidgets cmake needs headers listed too for the resource system.)
- [ ] **Step 4: Commit**
```bash
git add src/slic3r/GUI/MainFrame.cpp src/slic3r/CMakeLists.txt
git commit -m "feat: wire wxInspector plugins into MainFrame and build
- Call RegisterOrcaInspectorPlugins() after SetupInspectorAccelerator
- Add all plugin source files to SLIC3R_GUI_SOURCES"
```
---
### Task 6: Build and verify
**Files:**
- None modified (verification only)
- [ ] **Step 1: Configure the build**
```powershell
$cmakePath = "D:\VisualStudio\2026\Community\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe"
& $cmakePath --build . --config Debug --target ALL_BUILD -- -m
```
Expected: Build succeeds with zero errors and zero warnings from our new files.
- [ ] **Step 2: Fix any compilation errors**
If the build fails:
- Check that `#include` paths resolve (the `slic3r/GUI/…` relative paths use `src/` as the include root — verify this is set up in CMake via `include_directories`)
- Check that `ButtonStyle` and `ButtonType` enums are visible (they're defined in `Button.hpp`)
- Check that `StateColor` constructor from `wxColour` is valid (it has `StateColor(wxColour const&)`)
- Check that `LabeledStaticBox::GetBorderColor()` returns by value (StateColor copy is fine)
- On macOS: static box margin removal call needs `#ifdef __WXOSX__` guard
- [ ] **Step 3: Launch OrcaSlicer and verify inspector**
Launch the built OrcaSlicer, press Ctrl+Shift+I to open the inspector:
1. Select the MainFrame in the tree — verify "DPI Scaling" category appears with Scale Factor, Prev Scale Factor, EM Unit, Normal Font, Force Rescale
2. Select an Orca Button — verify "Orca Button" category appears
3. Select an Orca CheckBox — verify "Orca CheckBox" category appears
4. Edit a property value (e.g., Scale Factor) — verify the setter applies correctly
5. Select a LabeledStaticBox — verify corner radius, border width, border color, scale appear
- [ ] **Step 5: Commit (if fixes were needed) or mark complete**
```bash
git status
```
If clean: verification complete. If changes were made: `git add` and commit with fix message.
@@ -0,0 +1,102 @@
# Move `wxInspectable` into `DPIAware` — Design Spec
Date: 2026-07-23
Branch: `dev/layout-inspector`
## Overview
Move the `wxInspector::wxInspectable` base class from individual leaf classes (`DPIDialog`, `MainFrame`) into the common `DPIAware<P>` template. This makes every DPIAware widget automatically visible in the inspector tree without requiring each subclass to opt in.
## Motivation
Currently, only `DPIDialog` and `MainFrame` explicitly inherit `wxInspectable`. `DPIFrame` (which `MainFrame` inherits from) does not — `MainFrame` adds it manually. This means:
- Any `DPIAware<T>` widget that isn't `DPIDialog` or `MainFrame` is invisible in the inspector tree
- `DPIFrame` subclasses (`BaseTransparentDPIFrame`, `ImageDPIFrame`, `ModelMallDialog`, `MediaFileFrame`, `SecondaryCheckDialog`, `PrintErrorDialog`, etc.) don't appear
- Adding a new DPIAware widget type requires remembering to also inherit `wxInspectable`
Moving `wxInspectable` to `DPIAware` fixes this for all current and future DPIAware widgets at once.
## Design
### Change 1: `GUI_Utils.hpp` — `DPIAware<P>`
Add `wxInspector::wxInspectable` as a second base class, and call `SetupInspectorAccelerator(this)` in the constructor (after `this->CenterOnParent()`):
```cpp
// Before:
template<class P> class DPIAware : public P
// After:
template<class P> class DPIAware : public P, public wxInspector::wxInspectable
```
Add in the constructor body (after `this->CenterOnParent()` at line 110):
```cpp
SetupInspectorAccelerator(this);
```
This gives every `DPIAware<T>` widget both inspectability and the Ctrl+Shift+I keyboard shortcut automatically. `#include <wx/inspector/inspector.h>` is already present in the file.
### Change 2: `GUI_Utils.hpp` — `DPIDialog`
Remove the now-redundant `wxInspector::wxInspectable` and the `SetupInspectorAccelerator(this)` call:
```cpp
// Before:
class DPIDialog : public DPIAware<wxDialog>, public wxInspector::wxInspectable
// ...
SetupInspectorAccelerator(this);
// After:
class DPIDialog : public DPIAware<wxDialog>
// (SetupInspectorAccelerator call removed — now done in DPIAware constructor)
```
`DPIDialog` gets `wxInspectable` and the accelerator through `DPIAware<wxDialog>` now.
### Change 3: `MainFrame.hpp` — `MainFrame`
Remove the now-redundant `wxInspector::wxInspectable`:
```cpp
// Before:
class MainFrame : public DPIFrame, public wxInspector::wxInspectable
// After:
class MainFrame : public DPIFrame
```
`MainFrame` gets `wxInspectable` through `DPIFrame` → `DPIAware<wxFrame>`.
### Change 4: `MainFrame.cpp` — `MainFrame` constructor
Remove the now-redundant `SetupInspectorAccelerator(this)` call (line 304). It will be called automatically by the `DPIAware` constructor.
## Impact
| Widget | Before | After |
|--------|--------|-------|
| `DPIDialog` subclasses (~80) | ✓ inspectable | ✓ inspectable (transitive) |
| `MainFrame` | ✓ inspectable | ✓ inspectable (transitive) |
| `DPIFrame` subclasses (8 others) | ✗ invisible | ✓ inspectable |
| Future `DPIAware<T>` | ✗ invisible | ✓ inspectable |
## Files Modified
| File | Change |
|------|--------|
| `src/slic3r/GUI/GUI_Utils.hpp` | `DPIAware<P>` gains `wxInspector::wxInspectable` + `SetupInspectorAccelerator(this)` call; `DPIDialog` drops redundant `wxInspector::wxInspectable` and `SetupInspectorAccelerator(this)` |
| `src/slic3r/GUI/MainFrame.hpp` | `MainFrame` drops redundant `wxInspector::wxInspectable` |
| `src/slic3r/GUI/MainFrame.cpp` | Remove redundant `SetupInspectorAccelerator(this)` from MainFrame constructor |
## Non-Goals
- The `DPIAwarePlugin` detection logic (`dynamic_cast<DPIFrame*>` / `dynamic_cast<DPIDialog*>`) is unchanged
- No new DPI properties — this is purely about tree visibility and accelerator setup
## Risk Assessment
- **Multiple inheritance**: `DPIAware<P>` already has a vtable (virtual destructor). Adding `wxInspectable` adds a second base but no additional data members. The `wxInspector::wxInspectable` class is expected to be a lightweight marker interface.
- **Build**: No new includes needed; `<wx/inspector/inspector.h>` is already included in `GUI_Utils.hpp`.
- **Cross-platform**: The change is standard C++ multiple inheritance — no platform-specific concerns.
@@ -0,0 +1,244 @@
# wxInspector Plugins for OrcaSlicer Custom Controls — Design Spec
Date: 2026-07-23
Branch: `dev/layout-inspector`
## Overview
Create wxInspector plugins that expose OrcaSlicer's custom widget properties in the inspector's property grid. Without these plugins, the inspector shows only generic wxWidgets properties — missing all DPI-awareness data, custom styling, and Orca-specific control state.
## Goals
1. **DPIAware properties** — Inspect and update `scale_factor`, `prev_scale_factor`, `em_unit`, and `normal_font` on any DPIAware-derived widget
2. **Custom widget properties** — Surface Orca-specific properties on `Button`, `CheckBox`, `TextInput`, `SwitchButton`, `ProgressBar`, `Label`, and `LabeledStaticBox`
3. **Minimal source changes** — Only add trivial (one-line) getters/setters to existing classes; no architectural refactoring of Orca's widget hierarchy
## Non-Goals
- Custom inspector panels or AUI tabs (use the existing property grid and method invoker)
- Python-plugin integration (this is C++ wxInspector, not Orca's Python plugin system)
- Event logging customization (the built-in event logger already works)
## Architecture
### Two Plugins
| Plugin | Class | Files |
|--------|-------|-------|
| DPIAware plugin | `DPIAwarePlugin` | `DPIAwarePlugin.hpp`, `DPIAwarePlugin.cpp` |
| Custom widgets plugin | `CustomWidgetsPlugin` | `CustomWidgetsPlugin.hpp`, `CustomWidgetsPlugin.cpp` |
| Registration helper | inline function | `Registration.hpp` |
All files live under `src/slic3r/Utils/wxInspectorPlugins/`.
### Plugin Detection Strategy
**DPIAware plugin**: Uses `dynamic_cast<DPIFrame*>` and `dynamic_cast<DPIDialog*>` as detection gates. `DPIFrame` = `DPIAware<wxFrame>`, `DPIDialog` = `DPIAware<wxDialog>`. Since these are concrete typedefs, `dynamic_cast` works at runtime. This covers `MainFrame`, `SettingsDialog`, and all 8 calibration dialogs (which inherit `DPIDialog`).
**Custom widgets plugin**: Gates broadly on `CLASSINFO(wxWindow)`, then uses per-type `dynamic_cast` inside `GetProperties` to check each Orca-specific type. Only matching types append properties.
### Registration
A single `RegisterOrcaInspectorPlugins()` inline function in `Registration.hpp` creates both plugins as function-local statics (matching the wxInspector built-in provider pattern) and registers them via `wxInspector::RegisterPlugin()`.
Called once from `MainFrame::MainFrame()` after `SetupInspectorAccelerator(this)`.
### Why Separate Plugins?
- DPIAware is a C++ template concept (not a wxClassInfo-isKindOf check), so it needs its own detection logic
- Custom widgets use standard wxClassInfo-based detection, matching the built-in provider pattern
- Two focused files are easier to review and maintain than one monolithic plugin
- Compile-time failure isolation: if a widget header changes, only one plugin breaks
## DPIAware Plugin — Property Specification
### Source Changes (GUI_Utils.hpp)
Four one-liner methods added to the `DPIAware<P>` template class (public section):
```cpp
float scale_factor() const { return m_scale_factor; } // already exists
float prev_scale_factor() const { return m_prev_scale_factor; } // already exists
int em_unit() const { return m_em_unit; } // already exists
void set_scale_factor(float v) { m_scale_factor = v; } // NEW
void set_prev_scale_factor(float v) { m_prev_scale_factor = v; } // NEW
void set_em_unit(int v) { m_em_unit = v; } // NEW
bool force_rescale() const { return m_force_rescale; } // NEW
// m_normal_font getter already exists: normal_font()
```
### Detection
```cpp
bool CanProvideProperties(wxClassInfo* info) override {
// Gated in GetProperties via dynamic_cast on the window itself
return info->IsKindOf(CLASSINFO(wxWindow));
}
```
In `GetProperties`:
```cpp
auto* win = obj.AsWindow();
bool isDPI = dynamic_cast<DPIFrame*>(win) || dynamic_cast<DPIDialog*>(win);
if (!isDPI) return props;
```
### Property Table (category: "DPI Scaling")
| Name | Type | Editable | Getter | Setter |
|------|------|----------|--------|--------|
| Scale Factor | String (float) | Yes | `dpi->scale_factor()` | `dpi->set_scale_factor(v)` |
| Prev Scale Factor | String (float) | Yes | `dpi->prev_scale_factor()` | `dpi->set_prev_scale_factor(v)` |
| EM Unit | Integer | Yes | `dpi->em_unit()` | `dpi->set_em_unit(v)` |
| Normal Font | ReadOnly | No | `dpi->normal_font().GetNativeFontInfoDesc()` | — |
| Force Rescale | Boolean (ReadOnly) | No | `dpi->force_rescale()` | — |
**Note on setters**: The setters simply store values. They do NOT trigger a widget rescale/layout. To see the effect of a changed scale factor, use the inspector's Methods panel to call `Layout()` or resize the window — which triggers the DPI_CHANGED event path naturally.
## Custom Widgets Plugin — Property Specification
All properties are appended to the built-in wxWindow properties. Each widget type is independently detected via `dynamic_cast`.
### Detection gates (in `GetProperties`)
```cpp
auto* win = obj.AsWindow();
if (auto* btn = dynamic_cast<Button*>(win)) { addButtonProperties(btn, props); }
if (auto* cb = dynamic_cast<CheckBox*>(win)) { addCheckBoxProperties(cb, props); }
if (auto* ti = dynamic_cast<TextInput*>(win)) { addTextInputProperties(ti, props); }
if (auto* sb = dynamic_cast<SwitchButton*>(win)) { addSwitchButtonProperties(sb, props); }
if (auto* pb = dynamic_cast<ProgressBar*>(win)) { addProgressBarProperties(pb, props); }
if (auto* lbl = dynamic_cast<Label*>(win)) { addLabelProperties(lbl, props); }
if (auto* lsb = dynamic_cast<LabeledStaticBox*>(win)) { addLabeledStaticBoxProperties(lsb, props); }
```
### Orca Button (`Button`) — category: "Orca Button"
| Name | Type | Editable | Getter | Setter |
|------|------|----------|--------|--------|
| Button Style | Choice | Yes | enum→string | string→enum |
| Button Type | Choice | Yes | enum→string | string→enum |
| Selected | Boolean | Yes | `m_selected` (needs getter) | `SetSelected(v)` |
| Active Icon | ReadOnly | No | icon name string | — |
| Inactive Icon | ReadOnly | No | icon name string | — |
Choices for Button Style: `Regular`, `Confirm`, `Alert`, `Disabled`
Choices for Button Type: `Compact`, `Window`, `Choice`, `Parameter`, `Icon`, `Expanded`
**Source changes needed**: Button's `m_selected` is private. Add one-liner getter:
```cpp
bool IsSelected() const { return m_selected; }
```
### Orca CheckBox (`CheckBox`) — category: "Orca CheckBox"
| Name | Type | Editable | Getter | Setter |
|------|------|----------|--------|--------|
| Half Checked | Boolean | Yes | `m_half_checked` (needs getter) | `SetHalfChecked(v)` |
**Source changes needed**: `m_half_checked` is private. Add one-liner getter:
```cpp
bool IsHalfChecked() const { return m_half_checked; }
```
### Orca TextInput (`TextInput`) — category: "Orca TextInput"
| Name | Type | Editable | Getter | Setter |
|------|------|----------|--------|--------|
| Label | String | Yes | `GetLabel()` (inherited from wxWindow) | `SetLabel(v)` (exists) |
| Text Value | String | Yes | `GetTextCtrl()->GetValue()` (GetTextCtrl is public) | `GetTextCtrl()->SetValue(v)` |
| Corner Radius | Integer | Yes | `GetCornerRadius()` (NEW) | `SetCornerRadius(v)` (exists) |
**Source changes needed**: Add one getter to `TextInput`:
```cpp
int GetCornerRadius() const { return static_cast<int>(radius); }
```
(`radius` is inherited from StaticBox. `SetCornerRadius(double)` already exists. `GetTextCtrl()` is already public.)
### Orca SwitchButton (`SwitchButton`) — category: "Orca SwitchButton"
| Name | Type | Editable | Getter | Setter |
|------|------|----------|--------|--------|
| Value | Boolean | Yes | existing getter | existing setter |
### Orca ProgressBar (`ProgressBar`) — category: "Orca ProgressBar"
| Name | Type | Editable | Getter | Setter |
|------|------|----------|--------|--------|
| Proportion | Float (0-1) | Yes | `pb->m_proportion` (public member) | `pb->m_proportion = v` |
| Show Number | Boolean | Yes | `pb->m_shownumber` (public member) | `pb->m_shownumber = v` |
**No source changes needed**: `m_proportion` and `m_shownumber` are already public members. `SetValue(int)` and `SetProgress(int)` already exist as public methods.
### Orca Label (`Label`) — category: "Orca Label"
| Name | Type | Editable | Getter | Setter |
|------|------|----------|--------|--------|
| Is Hyperlink | Boolean | No | existing flag check | — |
| Font Size | ReadOnly | No | `GetFont().GetPointSize()` | — |
### LabeledStaticBox — category: "LabeledStaticBox"
| Name | Type | Editable | Getter | Setter |
|------|------|----------|--------|--------|
| Corner Radius | Integer | Yes | `GetCornerRadius()` (NEW) | `SetCornerRadius(v)` (exists) |
| Border Width | Integer | Yes | `GetBorderWidth()` (NEW) | `SetBorderWidth(v)` (exists) |
| Border Color | String (hex) | Yes | `GetBorderColor()` (NEW) | `SetBorderColor(v)` (exists) |
| Scale | Float (ReadOnly) | No | `m_scale` (protected, needs getter) | — |
**Source changes needed**: Four one-liner getters added to `LabeledStaticBox`:
```cpp
int GetCornerRadius() const { return m_radius; }
int GetBorderWidth() const { return m_border_width; }
StateColor GetBorderColor() const { return border_color; }
float GetScale() const { return m_scale; }
```
## Files Modified (Existing Code)
| File | Changes |
|------|---------|
| `src/slic3r/GUI/GUI_Utils.hpp` | +4 methods in `DPIAware<P>`: `set_scale_factor()`, `set_prev_scale_factor()`, `set_em_unit()`, `force_rescale()` |
| `src/slic3r/GUI/Widgets/LabeledStaticBox.hpp` | +4 getter declarations: `GetCornerRadius()`, `GetBorderWidth()`, `GetBorderColor()`, `GetScale()` |
| `src/slic3r/GUI/Widgets/LabeledStaticBox.cpp` | +4 getter implementations |
| `src/slic3r/GUI/Widgets/Button.hpp` | +1 getter: `IsSelected()` |
| `src/slic3r/GUI/Widgets/CheckBox.hpp` | +1 getter: `IsHalfChecked()` |
| `src/slic3r/GUI/Widgets/TextInput.hpp` | +1 getter: `GetCornerRadius()` |
| `src/slic3r/GUI/Widgets/ProgressBar.hpp` | None (public members are used directly) |
| `src/slic3r/GUI/MainFrame.cpp` | +1 `#include`, +1 call to `RegisterOrcaInspectorPlugins()` |
| `src/slic3r/CMakeLists.txt` | +4 entries in `SLIC3R_GUI_SOURCES` (the .cpp plugin files) |
## Files Created
```
src/slic3r/Utils/wxInspectorPlugins/
├── DPIAwarePlugin.hpp
├── DPIAwarePlugin.cpp
├── CustomWidgetsPlugin.hpp
├── CustomWidgetsPlugin.cpp
└── Registration.hpp
```
## Build & Linking
The `wxInspector` dependency is already wired:
- `deps/wxInspector/wxInspector.cmake` fetches and builds wxInspector
- `src/CMakeLists.txt` lines 92-93 link `wxInspector::wxInspector` into `wxWidgets_LIBRARIES`
- The plugin files only need `#include <wx/inspector/plugin.h>` and `#include <wx/inspector/inspector.h>` — both available from the installed dependency
No new CMake dependencies needed. Only the new source files need listing in `SLIC3R_GUI_SOURCES`.
## Error Handling & Edge Cases
- **Stale pointers**: Plugin lambdas capture raw pointers, regenerated on every `GetProperties` call (matching wxInspector's built-in provider pattern). Pointers live only until the next tree selection.
- **Widget destruction**: If a widget is destroyed while the inspector is showing its properties, `InspectableObject::IsValid()` returns false and properties are not displayed. The inspector won't show stale data.
- **Invalid property values**: Setters use `sscanf` / `ToLong` with validation (matching built-in patterns). Bogus input is rejected — setter returns `false`, property grid shows error state.
- **DPI drift**: Setting `scale_factor` without triggering rescale means displayed sizes don't match the new factor. This is acceptable — the inspector is a developer tool; operators know to call `Layout()` after making changes.
- **Missing widget type**: If a `dynamic_cast` fails for all types, only built-in wxWindow properties are shown. No crash, no error — just reduced info.
## Future Work (Out of Scope)
- **StateColor visualization**: `StateColor` is a multi-value type (maps bitmask states to colors). A full solution would need a custom property editor (e.g., a table showing each state→color pair). Keep it simple for now.
- **ScalableBitmap display**: Could show the bitmap as an inline thumbnail. Complex property editor work — deferred.
- **More widget types**: `SwitchBoard`, `MultiSwitchButton`, `StepCtrl`, `FanControl`, `DropDown`, `ComboBox`, `AMS*` widgets could all benefit. Add as needed.
- **Property refresh on tree selection**: Currently properties are static snapshots. A "refresh" button or auto-poll could keep values current for rapidly-changing widgets (progress bars, etc.). The built-in wxInspector already provides a tree-refresh button.
+95 -12
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-23 15:24-0300\n"
"POT-Creation-Date: 2026-07-29 17:40-0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -982,7 +982,7 @@ msgstr ""
#, possible-boost-format
msgid ""
"Objects(%1%) have duplicated connectors. Some connectors may be missing in slicing result.\n"
"Please report to PrusaSlicer team in which scenario this issue happened.\n"
"Please report to the OrcaSlicer team in which scenario this issue happened.\n"
"Thank you."
msgstr ""
@@ -3385,7 +3385,6 @@ msgstr ""
msgid "Innerloop"
msgstr ""
#. TRN To be shown in the main menu View->Top
msgid "Top"
msgstr ""
@@ -3609,6 +3608,9 @@ msgstr ""
msgid "Arranging"
msgstr ""
msgid "Arranging "
msgstr ""
msgid "Arranging canceled."
msgstr ""
@@ -5470,10 +5472,24 @@ msgstr ""
msgid "Align to Y axis"
msgstr ""
msgctxt "Camera View"
msgid "Front"
msgstr ""
msgctxt "Camera View"
msgid "Back"
msgstr ""
#. TRN To be shown in the main menu View->Top
msgctxt "Camera View"
msgid "Top"
msgstr ""
#. TRN To be shown in the main menu View->Bottom
msgctxt "Camera View"
msgid "Bottom"
msgstr ""
msgctxt "Camera View"
msgid "Left"
msgstr ""
@@ -5854,19 +5870,13 @@ msgstr ""
msgid "Top View"
msgstr ""
#. TRN To be shown in the main menu View->Bottom
msgid "Bottom"
msgstr ""
msgid "Bottom View"
msgstr ""
msgid "Front"
msgstr ""
msgid "Front View"
msgstr ""
msgctxt "Camera View"
msgid "Rear"
msgstr ""
@@ -8654,6 +8664,17 @@ msgstr ""
msgid "When scrubbing the layer slider in the sliced preview, render the layers below the current one darkened so that only the layer being viewed is shown at full brightness."
msgstr ""
msgid "Dimmed layer brightness"
msgstr ""
msgid "%"
msgstr ""
msgid ""
"How brightly the dimmed layers are rendered when \"Dim lower layers\" is enabled.\n"
"99% is barely darkened, 0% renders them black. Capped at 99% because 100% would be the same as disabling the option."
msgstr ""
msgid "Login region"
msgstr ""
@@ -12350,12 +12371,26 @@ msgstr ""
msgid "Intra-layer order"
msgstr ""
msgid "Print order within a single layer."
msgid ""
"Order in which object instances are visited within a single layer, which controls how much travel is spent moving between them.\n"
"\n"
"Default: nearest-neighbor chaining, refined with 2-opt and crossing removal. A good general choice.\n"
"As object list: instances are printed in the same order as the object list, without any path optimization. Use it when you need a predictable, manually controlled order.\n"
"Best of all (shortest path): every strategy is evaluated and the shortest one is used. The object instance order is decided once for the whole print, while the ordering of individual islands is decided per layer, so different layers may end up using different strategies. Slightly slower to slice.\n"
"Snake: serpentine row-by-row traversal, refined with 2-opt. Well suited to regular grids of many small parts.\n"
"\n"
"With multiple filaments or tools in the same layer, minimizing tool changes takes priority: objects are grouped by filament first and this setting only orders the instances within each filament group, so the overall sequence may not look like the shortest path across the plate."
msgstr ""
msgid "As object list"
msgstr ""
msgid "Best of all (shortest path)"
msgstr ""
msgid "Snake"
msgstr ""
msgid "Slow printing down for better layer cooling"
msgstr ""
@@ -14729,7 +14764,7 @@ msgstr ""
msgid "Retract amount after wipe"
msgstr ""
#, possible-c-format
#, no-c-format, no-boost-format
msgid ""
"The length of fast retraction after wipe, relative to retraction length.\n"
"The value will be clamped by 100% minus the retract amount before the wipe value."
@@ -17211,6 +17246,15 @@ msgid ""
"Please select one that should be used."
msgstr ""
msgid "Auto-scale for nozzle"
msgstr ""
msgid ""
"This model is designed around a 0.4 mm nozzle with a 0.2 mm layer height. \n"
"When the scaling option is enabled (recommended), it dynamically resizes to match your current nozzle diameter and an appropriate layer height, making the test both accurate and easy to read.\n"
"Turn scaling off only if you wish to print the reference model exactly as-is."
msgstr ""
msgid "PA Calibration"
msgstr ""
@@ -17333,6 +17377,12 @@ msgstr ""
msgid "End speed: "
msgstr ""
msgid "Auto-adjust to max volumetric speed"
msgstr ""
msgid "If the end speed would exceed the filament's maximum volumetric speed, automatically lower the layer height (keeping standard values and staying within the machine's limits) to reach it. If even the minimum layer height is not enough, lower the end speed instead."
msgstr ""
msgid ""
"Please input valid values:\n"
"start > 10\n"
@@ -17340,6 +17390,39 @@ msgid ""
"end > start + step"
msgstr ""
#, possible-c-format, possible-boost-format
msgid ""
"The end speed (%.0f mm/s) exceeds the filament's maximum volumetric speed (%.1f mm³/s), which limits the outer wall to about %.0f mm/s at this line width and layer height.\n"
" Speeds above this will be clamped, so the upper blocks of the tower will not print at the requested speed.\n"
"\n"
"%s"
msgstr ""
#, possible-c-format, possible-boost-format
msgid ""
"The end speed (%.0f mm/s) exceeds the filament's maximum volumetric speed (%.1f mm³/s) at the default layer height (%.2f mm).\n"
"\n"
"The layer height has been reduced to %.2f mm (a value used by this printer's profiles) so the tower can reach the requested speed."
msgstr ""
#, possible-c-format, possible-boost-format
msgid ""
"Even at the smallest layer height used by this printer's profiles (%.2f mm) the end speed (%.0f mm/s) exceeds the filament's maximum volumetric speed (%.1f mm³/s).\n"
"\n"
"The layer height will be set to %.2f mm and the end speed lowered to %.0f mm/s.\n"
"\n"
"Continue?"
msgstr ""
msgid "Continue anyway?"
msgstr ""
msgid "Enable \"Auto-adjust\" to fix this automatically, or continue anyway?"
msgstr ""
msgid "Enable \"Auto-scale for nozzle\" and \"Auto-adjust\" to fix this automatically, or continue anyway?"
msgstr ""
msgid "Start retraction length: "
msgstr ""
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+95 -12
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-23 15:24-0300\n"
"POT-Creation-Date: 2026-07-29 17:40-0300\n"
"PO-Revision-Date: 2026-06-17 15:44-0300\n"
"Last-Translator: Alexandre Folle de Menezes\n"
"Language-Team: \n"
@@ -978,7 +978,7 @@ msgstr ""
#, boost-format
msgid ""
"Objects(%1%) have duplicated connectors. Some connectors may be missing in slicing result.\n"
"Please report to PrusaSlicer team in which scenario this issue happened.\n"
"Please report to the OrcaSlicer team in which scenario this issue happened.\n"
"Thank you."
msgstr ""
@@ -3381,7 +3381,6 @@ msgstr ""
msgid "Innerloop"
msgstr ""
#. TRN To be shown in the main menu View->Top
msgid "Top"
msgstr ""
@@ -3605,6 +3604,9 @@ msgstr ""
msgid "Arranging"
msgstr ""
msgid "Arranging "
msgstr ""
msgid "Arranging canceled."
msgstr ""
@@ -5466,10 +5468,24 @@ msgstr ""
msgid "Align to Y axis"
msgstr ""
msgctxt "Camera View"
msgid "Front"
msgstr ""
msgctxt "Camera View"
msgid "Back"
msgstr ""
#. TRN To be shown in the main menu View->Top
msgctxt "Camera View"
msgid "Top"
msgstr ""
#. TRN To be shown in the main menu View->Bottom
msgctxt "Camera View"
msgid "Bottom"
msgstr ""
msgctxt "Camera View"
msgid "Left"
msgstr ""
@@ -5850,19 +5866,13 @@ msgstr ""
msgid "Top View"
msgstr ""
#. TRN To be shown in the main menu View->Bottom
msgid "Bottom"
msgstr ""
msgid "Bottom View"
msgstr ""
msgid "Front"
msgstr ""
msgid "Front View"
msgstr ""
msgctxt "Camera View"
msgid "Rear"
msgstr ""
@@ -8650,6 +8660,17 @@ msgstr ""
msgid "When scrubbing the layer slider in the sliced preview, render the layers below the current one darkened so that only the layer being viewed is shown at full brightness."
msgstr ""
msgid "Dimmed layer brightness"
msgstr ""
msgid "%"
msgstr ""
msgid ""
"How brightly the dimmed layers are rendered when \"Dim lower layers\" is enabled.\n"
"99% is barely darkened, 0% renders them black. Capped at 99% because 100% would be the same as disabling the option."
msgstr ""
msgid "Login region"
msgstr ""
@@ -12346,12 +12367,26 @@ msgstr ""
msgid "Intra-layer order"
msgstr ""
msgid "Print order within a single layer."
msgid ""
"Order in which object instances are visited within a single layer, which controls how much travel is spent moving between them.\n"
"\n"
"Default: nearest-neighbor chaining, refined with 2-opt and crossing removal. A good general choice.\n"
"As object list: instances are printed in the same order as the object list, without any path optimization. Use it when you need a predictable, manually controlled order.\n"
"Best of all (shortest path): every strategy is evaluated and the shortest one is used. The object instance order is decided once for the whole print, while the ordering of individual islands is decided per layer, so different layers may end up using different strategies. Slightly slower to slice.\n"
"Snake: serpentine row-by-row traversal, refined with 2-opt. Well suited to regular grids of many small parts.\n"
"\n"
"With multiple filaments or tools in the same layer, minimizing tool changes takes priority: objects are grouped by filament first and this setting only orders the instances within each filament group, so the overall sequence may not look like the shortest path across the plate."
msgstr ""
msgid "As object list"
msgstr ""
msgid "Best of all (shortest path)"
msgstr ""
msgid "Snake"
msgstr ""
msgid "Slow printing down for better layer cooling"
msgstr ""
@@ -14725,7 +14760,7 @@ msgstr ""
msgid "Retract amount after wipe"
msgstr ""
#, c-format
#, no-c-format, no-boost-format
msgid ""
"The length of fast retraction after wipe, relative to retraction length.\n"
"The value will be clamped by 100% minus the retract amount before the wipe value."
@@ -17207,6 +17242,15 @@ msgid ""
"Please select one that should be used."
msgstr ""
msgid "Auto-scale for nozzle"
msgstr ""
msgid ""
"This model is designed around a 0.4 mm nozzle with a 0.2 mm layer height. \n"
"When the scaling option is enabled (recommended), it dynamically resizes to match your current nozzle diameter and an appropriate layer height, making the test both accurate and easy to read.\n"
"Turn scaling off only if you wish to print the reference model exactly as-is."
msgstr ""
msgid "PA Calibration"
msgstr ""
@@ -17329,6 +17373,12 @@ msgstr ""
msgid "End speed: "
msgstr ""
msgid "Auto-adjust to max volumetric speed"
msgstr ""
msgid "If the end speed would exceed the filament's maximum volumetric speed, automatically lower the layer height (keeping standard values and staying within the machine's limits) to reach it. If even the minimum layer height is not enough, lower the end speed instead."
msgstr ""
msgid ""
"Please input valid values:\n"
"start > 10\n"
@@ -17336,6 +17386,39 @@ msgid ""
"end > start + step"
msgstr ""
#, c-format, boost-format
msgid ""
"The end speed (%.0f mm/s) exceeds the filament's maximum volumetric speed (%.1f mm³/s), which limits the outer wall to about %.0f mm/s at this line width and layer height.\n"
" Speeds above this will be clamped, so the upper blocks of the tower will not print at the requested speed.\n"
"\n"
"%s"
msgstr ""
#, c-format, boost-format
msgid ""
"The end speed (%.0f mm/s) exceeds the filament's maximum volumetric speed (%.1f mm³/s) at the default layer height (%.2f mm).\n"
"\n"
"The layer height has been reduced to %.2f mm (a value used by this printer's profiles) so the tower can reach the requested speed."
msgstr ""
#, c-format, boost-format
msgid ""
"Even at the smallest layer height used by this printer's profiles (%.2f mm) the end speed (%.0f mm/s) exceeds the filament's maximum volumetric speed (%.1f mm³/s).\n"
"\n"
"The layer height will be set to %.2f mm and the end speed lowered to %.0f mm/s.\n"
"\n"
"Continue?"
msgstr ""
msgid "Continue anyway?"
msgstr ""
msgid "Enable \"Auto-adjust\" to fix this automatically, or continue anyway?"
msgstr ""
msgid "Enable \"Auto-scale for nozzle\" and \"Auto-adjust\" to fix this automatically, or continue anyway?"
msgstr ""
msgid "Start retraction length: "
msgstr ""
+154 -17
View File
@@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Orca Slicer\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-23 15:24-0300\n"
"POT-Creation-Date: 2026-07-29 17:40-0300\n"
"PO-Revision-Date: \n"
"Last-Translator: Ian A. Bassi <>\n"
"Language-Team: \n"
@@ -983,11 +983,11 @@ msgstr "Conector"
#, boost-format
msgid ""
"Objects(%1%) have duplicated connectors. Some connectors may be missing in slicing result.\n"
"Please report to PrusaSlicer team in which scenario this issue happened.\n"
"Please report to the OrcaSlicer team in which scenario this issue happened.\n"
"Thank you."
msgstr ""
"Los objetos(%1%) tienen conectores duplicados. Es posible que falten algunos conectores en el resultado del laminado.\n"
"Informe al equipo de PrusaSlicer sobre el escenario en el que se produjo este problema.\n"
"Informe al equipo de OrcaSlicer sobre el escenario en el que se produjo este problema.\n"
"Gracias."
msgid "Cut by Plane"
@@ -3459,7 +3459,6 @@ msgstr "Recámara"
msgid "Innerloop"
msgstr "Bucle interno"
#. TRN To be shown in the main menu View->Top
msgid "Top"
msgstr "Superior"
@@ -3687,6 +3686,10 @@ msgstr "Organizando..."
msgid "Arranging"
msgstr "Organizando"
# AI Translated
msgid "Arranging "
msgstr "Organizando "
msgid "Arranging canceled."
msgstr "Organización cancelada."
@@ -5630,10 +5633,27 @@ msgstr "Evitar la zona de calibración del extrusor"
msgid "Align to Y axis"
msgstr "Alinear con el eje Y"
# AI Translated
msgctxt "Camera View"
msgid "Front"
msgstr "Frontal"
msgctxt "Camera View"
msgid "Back"
msgstr "Posterior"
# AI Translated
#. TRN To be shown in the main menu View->Top
msgctxt "Camera View"
msgid "Top"
msgstr "Superior"
# AI Translated
#. TRN To be shown in the main menu View->Bottom
msgctxt "Camera View"
msgid "Bottom"
msgstr "Inferior"
msgctxt "Camera View"
msgid "Left"
msgstr "Izquierda"
@@ -6020,19 +6040,14 @@ msgstr "Vista por Defecto"
msgid "Top View"
msgstr "Vista superior"
#. TRN To be shown in the main menu View->Bottom
msgid "Bottom"
msgstr "Inferior"
msgid "Bottom View"
msgstr "Vista inferior"
msgid "Front"
msgstr "Frontal"
msgid "Front View"
msgstr "Vista frontal"
# AI Translated
msgctxt "Camera View"
msgid "Rear"
msgstr "Posterior"
@@ -8920,6 +8935,21 @@ msgstr "Atenuar las capas inferiores"
msgid "When scrubbing the layer slider in the sliced preview, render the layers below the current one darkened so that only the layer being viewed is shown at full brightness."
msgstr "Al desplazar el control deslizante de capas en la vista previa laminada, oscurece las capas inferiores a la actual para que solo la capa visualizada se muestre a pleno brillo."
# AI Translated
msgid "Dimmed layer brightness"
msgstr "Brillo de las capas atenuadas"
msgid "%"
msgstr "%"
# AI Translated
msgid ""
"How brightly the dimmed layers are rendered when \"Dim lower layers\" is enabled.\n"
"99% is barely darkened, 0% renders them black. Capped at 99% because 100% would be the same as disabling the option."
msgstr ""
"Con qué brillo se muestran las capas atenuadas cuando \"Atenuar las capas inferiores\" está activado.\n"
"99% apenas se oscurece y 0% las muestra en negro. Está limitado al 99% porque el 100% equivaldría a desactivar la opción."
msgid "Login region"
msgstr "Región de inicio de sesión"
@@ -12870,12 +12900,37 @@ msgstr "Por objeto"
msgid "Intra-layer order"
msgstr "Orden dentro de la capa"
msgid "Print order within a single layer."
msgstr "Orden de impresión dentro de cada capa."
# AI Translated
msgid ""
"Order in which object instances are visited within a single layer, which controls how much travel is spent moving between them.\n"
"\n"
"Default: nearest-neighbor chaining, refined with 2-opt and crossing removal. A good general choice.\n"
"As object list: instances are printed in the same order as the object list, without any path optimization. Use it when you need a predictable, manually controlled order.\n"
"Best of all (shortest path): every strategy is evaluated and the shortest one is used. The object instance order is decided once for the whole print, while the ordering of individual islands is decided per layer, so different layers may end up using different strategies. Slightly slower to slice.\n"
"Snake: serpentine row-by-row traversal, refined with 2-opt. Well suited to regular grids of many small parts.\n"
"\n"
"With multiple filaments or tools in the same layer, minimizing tool changes takes priority: objects are grouped by filament first and this setting only orders the instances within each filament group, so the overall sequence may not look like the shortest path across the plate."
msgstr ""
"Orden en el que se recorren las instancias de objeto dentro de una misma capa, lo que determina cuánto desplazamiento se emplea para moverse entre ellas.\n"
"\n"
"Por defecto: encadenado por vecino más cercano, refinado con 2-opt y eliminación de cruces. Una buena opción general.\n"
"Como lista de objetos: las instancias se imprimen en el mismo orden que la lista de objetos, sin ninguna optimización de trayectoria. Úselo cuando necesite un orden predecible y controlado manualmente.\n"
"La mejor de todas (trayectoria más corta): se evalúan todas las estrategias y se utiliza la más corta. El orden de las instancias de objeto se decide una sola vez para toda la impresión, mientras que el orden de las islas individuales se decide capa por capa, por lo que distintas capas pueden acabar usando estrategias diferentes. El laminado es algo más lento.\n"
"Serpentina: recorrido serpenteante fila por fila, refinado con 2-opt. Muy adecuado para rejillas regulares de muchas piezas pequeñas.\n"
"\n"
"Con varios filamentos o herramientas en la misma capa, minimizar los cambios de herramienta tiene prioridad: los objetos se agrupan primero por filamento y este ajuste solo ordena las instancias dentro de cada grupo de filamento, por lo que la secuencia global puede no parecer la trayectoria más corta a lo largo de la cama."
msgid "As object list"
msgstr "Como lista de objetos"
# AI Translated
msgid "Best of all (shortest path)"
msgstr "La mejor de todas (trayectoria más corta)"
# AI Translated
msgid "Snake"
msgstr "Serpentina"
msgid "Slow printing down for better layer cooling"
msgstr "Reducir la velocidad de impresión para mejorar la refrigeración de las capas"
@@ -15490,7 +15545,7 @@ msgstr "La longitud de la retracción rápida antes de la purga, en relación co
msgid "Retract amount after wipe"
msgstr "Cantidad de retracción después de la limpieza"
#, c-format
#, no-c-format, no-boost-format
msgid ""
"The length of fast retraction after wipe, relative to retraction length.\n"
"The value will be clamped by 100% minus the retract amount before the wipe value."
@@ -18111,6 +18166,20 @@ msgstr ""
"Hay varias direcciones IP resueltas del nombre del host %1%.\n"
"Por favor, seleccione la que debe usarse."
# AI Translated
msgid "Auto-scale for nozzle"
msgstr "Escalar automáticamente para la boquilla"
# AI Translated
msgid ""
"This model is designed around a 0.4 mm nozzle with a 0.2 mm layer height. \n"
"When the scaling option is enabled (recommended), it dynamically resizes to match your current nozzle diameter and an appropriate layer height, making the test both accurate and easy to read.\n"
"Turn scaling off only if you wish to print the reference model exactly as-is."
msgstr ""
"Este modelo está diseñado para una boquilla de 0,4 mm con una altura de la capa de 0,2 mm. \n"
"Cuando la opción de escalado está activada (recomendado), el modelo se redimensiona dinámicamente para adaptarse al diámetro de boquilla actual y a una altura de capa adecuada, lo que hace que la prueba sea precisa y fácil de leer.\n"
"Desactive el escalado solo si desea imprimir el modelo de referencia exactamente tal cual."
msgid "PA Calibration"
msgstr "Calibración PA"
@@ -18247,6 +18316,14 @@ msgstr "Velocidad inicial: "
msgid "End speed: "
msgstr "Velocidad final: "
# AI Translated
msgid "Auto-adjust to max volumetric speed"
msgstr "Ajustar automáticamente a la velocidad volumétrica máxima"
# AI Translated
msgid "If the end speed would exceed the filament's maximum volumetric speed, automatically lower the layer height (keeping standard values and staying within the machine's limits) to reach it. If even the minimum layer height is not enough, lower the end speed instead."
msgstr "Si la velocidad final superara la velocidad volumétrica máxima del filamento, se reduce automáticamente la altura de la capa (manteniendo valores estándar y dentro de los límites de la máquina) para alcanzarla. Si ni siquiera la altura de capa mínima es suficiente, se reduce la velocidad final."
msgid ""
"Please input valid values:\n"
"start > 10\n"
@@ -18258,6 +18335,57 @@ msgstr ""
"incremento >=0\n"
"final > inicio + paso"
# AI Translated
#, c-format, boost-format
msgid ""
"The end speed (%.0f mm/s) exceeds the filament's maximum volumetric speed (%.1f mm³/s), which limits the outer wall to about %.0f mm/s at this line width and layer height.\n"
" Speeds above this will be clamped, so the upper blocks of the tower will not print at the requested speed.\n"
"\n"
"%s"
msgstr ""
"La velocidad final (%.0f mm/s) supera la velocidad volumétrica máxima del filamento (%.1f mm³/s), lo que limita el perímetro externo a unos %.0f mm/s con este ancho de línea y esta altura de capa.\n"
" Las velocidades superiores se recortarán, por lo que los bloques superiores de la torre no se imprimirán a la velocidad solicitada.\n"
"\n"
"%s"
# AI Translated
#, c-format, boost-format
msgid ""
"The end speed (%.0f mm/s) exceeds the filament's maximum volumetric speed (%.1f mm³/s) at the default layer height (%.2f mm).\n"
"\n"
"The layer height has been reduced to %.2f mm (a value used by this printer's profiles) so the tower can reach the requested speed."
msgstr ""
"La velocidad final (%.0f mm/s) supera la velocidad volumétrica máxima del filamento (%.1f mm³/s) con la altura de capa predeterminada (%.2f mm).\n"
"\n"
"La altura de la capa se ha reducido a %.2f mm (un valor usado por los perfiles de esta impresora) para que la torre pueda alcanzar la velocidad solicitada."
# AI Translated
#, c-format, boost-format
msgid ""
"Even at the smallest layer height used by this printer's profiles (%.2f mm) the end speed (%.0f mm/s) exceeds the filament's maximum volumetric speed (%.1f mm³/s).\n"
"\n"
"The layer height will be set to %.2f mm and the end speed lowered to %.0f mm/s.\n"
"\n"
"Continue?"
msgstr ""
"Incluso con la altura de capa más pequeña usada por los perfiles de esta impresora (%.2f mm), la velocidad final (%.0f mm/s) supera la velocidad volumétrica máxima del filamento (%.1f mm³/s).\n"
"\n"
"La altura de la capa se establecerá en %.2f mm y la velocidad final se reducirá a %.0f mm/s.\n"
"\n"
"¿Continuar?"
# AI Translated
msgid "Continue anyway?"
msgstr "¿Continuar de todos modos?"
# AI Translated
msgid "Enable \"Auto-adjust\" to fix this automatically, or continue anyway?"
msgstr "¿Activar \"Ajuste automático\" para corregir esto automáticamente o continuar de todos modos?"
# AI Translated
msgid "Enable \"Auto-scale for nozzle\" and \"Auto-adjust\" to fix this automatically, or continue anyway?"
msgstr "¿Activar \"Escalar automáticamente para la boquilla\" y \"Ajuste automático\" para corregir esto automáticamente o continuar de todos modos?"
msgid "Start retraction length: "
msgstr "Longitud de retracción inicial: "
@@ -20733,6 +20861,18 @@ msgstr ""
"Evita la deformación\n"
"¿Sabías que al imprimir materiales propensos a la deformación como el ABS, aumentar adecuadamente la temperatura de la cama térmica puede reducir la probabilidad de deformaciones?"
#~ msgid "Print order within a single layer."
#~ msgstr "Orden de impresión dentro de cada capa."
#~ msgid "Bottom"
#~ msgstr "Inferior"
#~ msgid "Front"
#~ msgstr "Frontal"
#~ msgid "Rear"
#~ msgstr "Posterior"
#~ msgid "Enter"
#~ msgstr "Enter"
@@ -20817,9 +20957,6 @@ msgstr ""
#~ msgid "°C"
#~ msgstr "°C"
#~ msgid "%"
#~ msgstr "%"
#~ msgid "Anisotropic surfaces"
#~ msgstr "Superficies anisótropas"
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -306,6 +306,12 @@ modules:
sha256: c08bc65a81971c1dd5783182826503369466c7e67374d1646519adf05207b684
dest: external-packages/python3
# wxInspector 1.0.0
- type: file
url: https://github.com/Noisyfox/wxInspector/archive/refs/tags/v1.0.0.zip
sha256: 0ba163956f2d468b19a91b96c5aba66ee9610843ea41dda628ea44cdafde7db7
dest: external-packages/wxInspector
# ---------------------------------------------------------------
# Fallback archives for deps normally provided by the GNOME SDK.
# These are only used if find_package() fails to locate them.
+17 -1
View File
@@ -2,11 +2,13 @@
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
xmlns:rescap3="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities/3"
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
xmlns:desktop6="http://schemas.microsoft.com/appx/manifest/desktop/windows10/6"
xmlns:virtualization="http://schemas.microsoft.com/appx/manifest/virtualization/windows10"
IgnorableNamespaces="uap rescap rescap3 desktop6 virtualization">
IgnorableNamespaces="uap uap3 rescap rescap3 desktop desktop6 virtualization">
<Identity Name="@MSIX_IDENTITY_NAME@"
Publisher="@MSIX_PUBLISHER@"
@@ -64,6 +66,20 @@
<uap:Extension Category="windows.protocol">
<uap:Protocol Name="orcaslicer" />
</uap:Extension>
<uap:Extension Category="windows.protocol">
<uap:Protocol Name="prusaslicer" />
</uap:Extension>
<uap:Extension Category="windows.protocol">
<uap:Protocol Name="bambustudio" />
</uap:Extension>
<uap:Extension Category="windows.protocol">
<uap:Protocol Name="cura" />
</uap:Extension>
<uap3:Extension Category="windows.appExecutionAlias" EntryPoint="Windows.FullTrustApplication">
<uap3:AppExecutionAlias>
<desktop:ExecutionAlias Alias="orca-slicer.exe" />
</uap3:AppExecutionAlias>
</uap3:Extension>
</Extensions>
</Application>
</Applications>
+4
View File
@@ -88,6 +88,10 @@ if (SLIC3R_GUI)
list(FILTER wxWidgets_LIBRARIES EXCLUDE REGEX OpenGL)
# list(REMOVE_ITEM wxWidgets_LIBRARIES oleacc)
find_package(wxInspector REQUIRED)
list(APPEND wxWidgets_LIBRARIES "wxInspector::wxInspector")
message(STATUS "wx libs: ${wxWidgets_LIBRARIES}")
add_subdirectory(slic3r)
+16 -1
View File
@@ -202,10 +202,25 @@ void AppConfig::set_defaults()
if (get("seq_top_layer_only").empty())
set("seq_top_layer_only", "1");
// ORCA: darken layers below the current one while scrubbing the preview (ported from preFlight)
// ORCA: darken the layers the preview layer slider is not scrubbed to
if (get("preview_dim_previous_layers").empty())
set_bool("preview_dim_previous_layers", false);
// ORCA: brightness of those dimmed layers, in percent. 0 = black, capped at 99 because
// 100 would render them unchanged, which is what disabling the option already does
if (get("preview_dim_previous_layers_brightness").empty())
set("preview_dim_previous_layers_brightness", "40");
else {
int brightness = 40;
try {
brightness = std::stoi(get("preview_dim_previous_layers_brightness"));
}
catch (...) {
brightness = 40;
}
set("preview_dim_previous_layers_brightness", std::to_string(std::max(0, std::min(brightness, 99))));
}
if (get("filaments_area_preferred_count").empty())
set("filaments_area_preferred_count", "10");
+14 -8
View File
@@ -32,15 +32,13 @@ static void append_and_translate(ExPolygons &dst, const ExPolygons &src, const P
for (; dst_idx < dst.size(); ++dst_idx)
dst[dst_idx].translate(instance_shift);
}
// BBS: generate brim area by objs
static void append_and_translate(ExPolygons& dst, const ExPolygons& src,
const PrintInstance& instance, size_t instance_idx, std::map<ObjectInstanceID, ExPolygons>& brimAreaMap) {
// Orca: Translate the brim area into print coordinates and store it per instance.
static void append_and_translate(const ExPolygons& src, const PrintInstance& instance,
size_t instance_idx, std::map<ObjectInstanceID, ExPolygons>& brimAreaMap) {
ExPolygons srcShifted = src;
Point instance_shift = instance.shift_without_plate_offset();
for (size_t src_idx = 0; src_idx < srcShifted.size(); ++src_idx)
srcShifted[src_idx].translate(instance_shift);
srcShifted = diff_ex(srcShifted, dst);
//expolygons_append(dst, temp2);
for (ExPolygon& expoly : srcShifted)
expoly.translate(instance_shift);
expolygons_append(brimAreaMap[{ instance.print_object->id(), instance_idx }], std::move(srcShifted));
}
@@ -572,7 +570,7 @@ static ExPolygons outer_inner_brim_area(const Print& print,
for (size_t instance_idx = 0; instance_idx < object->instances().size(); ++instance_idx) {
const PrintInstance& instance = object->instances()[instance_idx];
if (!brim_area_object.empty())
append_and_translate(brim_area, brim_area_object, instance, instance_idx, brimAreaMap);
append_and_translate(brim_area_object, instance, instance_idx, brimAreaMap);
append_and_translate(no_brim_area, no_brim_area_object, instance);
append_and_translate(holes, holes_object, instance);
append_and_translate(objectIslands, objectIsland, instance);
@@ -875,6 +873,14 @@ void make_brim(const Print& print, PrintTryCancel try_cancel, Polygons& islands_
ExPolygons islands_area_ex = outer_inner_brim_area(print,
float(flow.scaled_spacing()), brimAreaMap, objPrintVec, printExtruders);
if (!print.config().combine_brims) {
ExPolygons claimed_area;
for (auto& [_, areas] : brimAreaMap) {
areas = diff_ex(areas, claimed_area);
expolygons_append(claimed_area, areas);
}
}
// BBS: Find boundingbox of the first layer
for (const ObjectID printObjID : print.print_object_ids()) {
BoundingBox bbx;
+2
View File
@@ -248,6 +248,8 @@ set(lisbslic3r_sources
GCode/Thumbnails.hpp
GCode/ToolOrdering.cpp
GCode/ToolOrdering.hpp
GCode/OrderingStrategies.cpp
GCode/OrderingStrategies.hpp
GCode/WipeTower2.cpp
GCode/WipeTower2.hpp
GCode/WipeTower.cpp
+1 -1
View File
@@ -78,7 +78,7 @@ public:
Standard_Boolean UserBreak() override { return should_stop.load(); }
void Show(const Message_ProgressScope&, const Standard_Boolean) override {
std::cout << "Progress: " << GetPosition() << "%" << std::endl;
std::cout << "Progress: " << std::fixed << std::setprecision(2) << 100.0 * GetPosition() << "%" << std::endl;
}
private:
std::atomic<bool>& should_stop;
+178 -44
View File
@@ -15,6 +15,7 @@
#include "GCode/Thumbnails.hpp"
#include "GCode/WipeTower.hpp"
#include "ShortestPath.hpp"
#include "GCode/OrderingStrategies.hpp"
#include "Print.hpp"
#include "Utils.hpp"
#include "ClipperUtils.hpp"
@@ -2841,6 +2842,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
m_role_based_fan_marker_layer.fill(-1);
m_fan_mover.release();
m_ordering_cache.clear();
m_writer.set_is_bbl_machine(is_bbl_printers);
@@ -3161,11 +3163,20 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
// In non-sequential print, the printing extruders may have been modified by the extruder switches stored in Model::custom_gcode_per_print_z.
// Therefore initialize the printing extruders from there.
this->set_extruders(tool_ordering.all_extruders());
print_object_instances_ordering =
// By default, order object instances using a nearest neighbor search.
print.config().print_order == PrintOrder::Default ? chain_print_object_instances(print)
print_object_instances_ordering =
// By default, order object instances using nearest-neighbor chaining plus
// 2-opt and crossing-removal post-processing.
(print.config().print_order == PrintOrder::Default ? chain_print_object_instances(print)
// Snake: serpentine row traversal + 2-opt
: (print.config().print_order == PrintOrder::Snake ? chain_print_object_instances_snake(print)
// Best of all: run every strategy, pick the shortest total path
: (print.config().print_order == PrintOrder::BestOfStrategies ? chain_print_object_instances_best_of(print)
// Otherwise same order as the object list
: sort_object_instances_by_model_order(print);
: sort_object_instances_by_model_order(print))));
}
if (initial_extruder_id == (unsigned int)-1) {
// Nothing to print!
@@ -5658,7 +5669,9 @@ LayerResult GCode::process_layer(
//Calibration Layer-specific GCode
switch (print.calib_mode()) {
case CalibMode::Calib_PA_Tower: {
gcode += writer().set_pressure_advance(print.calib_params().start + static_cast<int>(print_z) * print.calib_params().step);
gcode += writer().set_pressure_advance(this->interpolate_value_across_layers(static_cast<float>(print.calib_params().start),
static_cast<float>(print.calib_params().end),
static_cast<float>(print.calib_params().step)));
break;
}
case CalibMode::Calib_Temp_Tower: {
@@ -5666,7 +5679,12 @@ LayerResult GCode::process_layer(
break;
}
case CalibMode::Calib_VFA_Tower: {
auto _speed = print.calib_params().start + std::floor(print_z / 5.0) * print.calib_params().step;
// Step the outer wall speed from start to end across the tower's layers. Plater::calib_VFA sizes the
// geometry so each speed step spans one visual block (a fixed number of layers), so the layer-based
// stepping stays aligned with the blocks regardless of nozzle size / layer height.
float _speed = this->interpolate_value_across_layers(static_cast<float>(print.calib_params().start),
static_cast<float>(print.calib_params().end),
static_cast<float>(print.calib_params().step));
m_calib_config.set_key_value("outer_wall_speed", new ConfigOptionFloatsNullable({std::round(_speed)}));
break;
}
@@ -6127,41 +6145,128 @@ LayerResult GCode::process_layer(
if (m_farthest_point_timelapse.enabled)
compute_farthest_point(layers, most_used_extruder, support_filaments);
std::map<unsigned int, std::vector<InstanceToPrint>> filament_to_print_instances;
// Per filament: instances to print, and the visit sequence over them. Island-level ordering
// may visit an instance more than once per layer; otherwise one visit per instance.
std::map<unsigned int, std::pair<std::vector<InstanceToPrint>, std::vector<InstanceVisit>>> filament_to_print_instances;
{
// Order individual islands rather than whole instances. Off for by-object sequencing,
// sequential printing, and the explicit AsObjectList order, which tour whole instances.
const bool island_level_ordering = print.config().print_sequence != PrintSequence::ByObject &&
single_object_instance_idx == size_t(-1) &&
print.config().print_order != PrintOrder::AsObjectList;
for (unsigned int filament_id : layer_tools.extruders) {
auto objects_by_extruder_it = by_extruder.find(filament_id);
if (objects_by_extruder_it == by_extruder.end()) continue;
auto &filament_plan = filament_to_print_instances[filament_id];
if (!island_level_ordering) {
// One visit per instance, printing all of its islands.
filament_plan.first = sort_print_object_instances(objects_by_extruder_it->second, layers, ordering, single_object_instance_idx);
filament_plan.second.reserve(filament_plan.first.size());
for (size_t i = 0; i < filament_plan.first.size(); ++i)
filament_plan.second.push_back({i, {}, true});
continue;
}
int plate_idx = print.get_plate_index();
Point wt_pos(print.config().wipe_tower_x.get_at(plate_idx), print.config().wipe_tower_y.get_at(plate_idx));
// Build the instances and one tour node per non-empty island (a single node for
// instances without chainable islands). Positions quantized to 1 mm so small
// centroid drift between layers still hits the tour cache below.
std::vector<GCode::ObjectByExtruder> &objects_by_extruder = objects_by_extruder_it->second;
std::vector<const PrintObject *> print_objects;
for (int obj_idx = 0; obj_idx < objects_by_extruder.size(); obj_idx++) {
auto &object_by_extruder = objects_by_extruder[obj_idx];
std::vector<InstanceToPrint> &instances = filament_plan.first;
std::vector<IslandOrderNode> nodes;
std::vector<size_t> node_instances;
auto quantize_to_mm = [](const Point &pt) -> Point {
const coord_t grid = coord_t(scale_(1.));
// Round to the nearest 1 mm symmetrically (integer division truncates toward
// zero, which would make the bucket straddling the origin twice as wide).
auto q = [grid](coord_t v) -> coord_t {
return ((v >= 0 ? v + grid / 2 : v - grid / 2) / grid) * grid;
};
return Point(q(pt.x()), q(pt.y()));
};
for (ObjectByExtruder &object_by_extruder : objects_by_extruder) {
if (object_by_extruder.islands.empty() && (object_by_extruder.support == nullptr || object_by_extruder.support->empty())) continue;
print_objects.push_back(print.get_object(obj_idx));
const size_t layer_id = &object_by_extruder - objects_by_extruder.data();
const PrintObject *print_object = layers[layer_id].original_object;
if (print_object == nullptr)
continue;
const Layer *obj_layer = layers[layer_id].object_layer;
std::vector<ObjectByExtruder::Island> &islands = object_by_extruder.islands;
const bool islands_chainable = obj_layer != nullptr && islands.size() == obj_layer->lslices.size() + 1;
for (size_t instance_id = 0; instance_id < print_object->instances().size(); ++instance_id) {
const size_t instance_idx = instances.size();
instances.emplace_back(object_by_extruder, layer_id, *print_object, instance_id,
print_object->instances()[instance_id].model_instance->get_labeled_id());
const Point &shift = print_object->instances()[instance_id].shift;
const size_t first_node = nodes.size();
if (islands_chainable)
for (size_t i = 0; i + 1 < islands.size(); ++i)
if (!islands[i].by_region.empty()) {
nodes.push_back({print_object->id(), instance_id, i,
quantize_to_mm(obj_layer->lslices[i].contour.centroid() + shift)});
node_instances.emplace_back(instance_idx);
}
if (nodes.size() == first_node) {
// No chainable islands: tour the whole instance as one stop.
nodes.push_back({print_object->id(), instance_id, size_t(-1), quantize_to_mm(shift)});
node_instances.emplace_back(instance_idx);
}
}
}
std::vector<const PrintInstance *> new_ordering = chain_print_object_instances(print_objects, &wt_pos);
std::reverse(new_ordering.begin(), new_ordering.end());
// Reuse the cached tour while this filament's island layout is unchanged.
auto &cache_entry = m_ordering_cache[filament_id];
if (!(cache_entry.first == nodes)) {
cache_entry.first = nodes;
Points node_points;
node_points.reserve(nodes.size());
for (const IslandOrderNode &node : nodes)
node_points.emplace_back(node.pos);
std::vector<size_t> tour = order_points_with_strategy(node_points, print.config().print_order, &wt_pos);
// Chained starting near the wipe tower, reversed so the layer ends near it.
std::reverse(tour.begin(), tour.end());
if (print.config().print_sequence == PrintSequence::ByObject) {
filament_to_print_instances[filament_id] = sort_print_object_instances(objects_by_extruder_it->second, layers, ordering, single_object_instance_idx);
} else {
// PrintSequence::ByLayer to use global ordering ( per object ordering ) if intra-layer order PrintOrder::AsObjectList is specified while keeping behaviour of PrintSequence::ByLayer
const std::vector<const PrintInstance*>* ordering_for_filament = (print.config().print_order == PrintOrder::AsObjectList && ordering != nullptr) ? ordering: &new_ordering;
filament_to_print_instances[filament_id] = sort_print_object_instances(objects_by_extruder_it->second, layers, ordering_for_filament, single_object_instance_idx);
// Group consecutive tour stops of the same instance into visits.
std::vector<InstanceVisit> visits;
std::vector<bool> instance_seen(instances.size(), false);
std::vector<int> last_visit_of_instance(instances.size(), -1);
for (size_t node_idx : tour) {
const size_t instance_idx = node_instances[node_idx];
if (visits.empty() || visits.back().instance_idx != instance_idx) {
visits.push_back({instance_idx, {}, !instance_seen[instance_idx]});
instance_seen[instance_idx] = true;
}
if (nodes[node_idx].island_idx != size_t(-1))
visits.back().islands.emplace_back(nodes[node_idx].island_idx);
last_visit_of_instance[instance_idx] = int(visits.size()) - 1;
}
// The trailing catch-all island has no geometry to chain by; append it to the
// instance's last visit.
for (size_t i = 0; i < instances.size(); ++i) {
if (last_visit_of_instance[i] < 0)
continue;
InstanceVisit &last_visit = visits[size_t(last_visit_of_instance[i])];
if (last_visit.islands.empty())
// A visit without explicit islands already prints everything.
continue;
std::vector<ObjectByExtruder::Island> &islands = instances[i].object_by_extruder.islands;
if (!islands.back().by_region.empty())
last_visit.islands.emplace_back(islands.size() - 1);
}
cache_entry.second = std::move(visits);
}
filament_plan.second = cache_entry.second;
}
}
std::set<size_t> layer_object_label_ids;
for (auto iter = filament_to_print_instances.begin(); iter != filament_to_print_instances.end(); ++iter) {
for (const InstanceToPrint &instance : iter->second) {
for (const InstanceToPrint &instance : iter->second.first) {
layer_object_label_ids.insert(instance.label_object_id);
}
}
@@ -6231,7 +6336,7 @@ LayerResult GCode::process_layer(
if (print.config().print_sequence == PrintSequence::ByLayer && m_enable_exclude_object && print.config().support_object_skip_flush.value) {
std::vector<size_t> filament_instances_id;
for (InstanceToPrint &instance : filament_to_print_instances[extruder_id]) filament_instances_id.emplace_back(instance.label_object_id);
for (InstanceToPrint &instance : filament_to_print_instances[extruder_id].first) filament_instances_id.emplace_back(instance.label_object_id);
m_filament_instances_code = _encode_label_ids_to_base64(filament_instances_id);
}
@@ -6312,7 +6417,9 @@ LayerResult GCode::process_layer(
if (layer_tools.has_wipe_tower && m_wipe_tower)
m_last_processor_extrusion_role = erWipeTower;
std::vector<InstanceToPrint> &instances_to_print = filament_to_print_instances[extruder_id];
auto &filament_plan = filament_to_print_instances[extruder_id];
std::vector<InstanceToPrint> &instances_to_print = filament_plan.first;
const std::vector<InstanceVisit> &instance_visits = filament_plan.second;
// We are almost ready to print. However, we must go through all the objects twice to print the overridden extrusions first (infill/perimeter wiping feature):
std::vector<ObjectByExtruder::Island::Region> by_region_per_copy_cache;
@@ -6320,10 +6427,11 @@ LayerResult GCode::process_layer(
if (is_anything_overridden && print_wipe_extrusions == 0)
gcode+="; PURGING FINISHED\n";
for (InstanceToPrint &instance_to_print : instances_to_print) {
for (const InstanceVisit &visit : instance_visits) {
InstanceToPrint &instance_to_print = instances_to_print[visit.instance_idx];
const auto& inst = instance_to_print.print_object.instances()[instance_to_print.instance_id];
const LayerToPrint &layer_to_print = layers[instance_to_print.layer_id];
if (print_wipe_extrusions == (is_anything_overridden ? 1 : 0)) {
if (visit.first_visit && print_wipe_extrusions == (is_anything_overridden ? 1 : 0)) {
gcode += generate_object_skirt_group(print, instance_to_print.print_object, instance_to_print.instance_id, layer_tools, layer, extruder_id);
gcode += generate_object_brim(print, instance_to_print.print_object, instance_to_print.instance_id, first_layer);
}
@@ -6376,7 +6484,7 @@ LayerResult GCode::process_layer(
m_avoid_crossing_perimeters.use_external_mp_once();
m_last_obj_copy = this_object_copy;
this->set_origin(unscale(offset));
if (instance_to_print.object_by_extruder.support != nullptr) {
if (visit.first_visit && instance_to_print.object_by_extruder.support != nullptr) {
m_layer = layers[instance_to_print.layer_id].support_layer;
m_object_layer_over_raft = false;
@@ -6410,9 +6518,42 @@ LayerResult GCode::process_layer(
m_layer = layer_to_print.layer();
m_object_layer_over_raft = object_layer_over_raft;
}
//FIXME order islands?
// Sequential tool path ordering of multiple parts within the same object, aka. perimeter tracking (#5511)
for (ObjectByExtruder::Island &island : instance_to_print.object_by_extruder.islands) {
// Island print order. Use the islands the tour assigned to this visit; if none,
// chain all islands nearest-neighbor from the current nozzle position (last_pos(),
// in this instance's frame after set_origin() above). Empty islands are skipped;
// the trailing catch-all island has no centroid to chain by and always goes last.
std::vector<ObjectByExtruder::Island> &islands = instance_to_print.object_by_extruder.islands;
std::vector<size_t> island_order = visit.islands;
if (island_order.empty()) {
island_order.reserve(islands.size());
if (layer_to_print.object_layer != nullptr && islands.size() == layer_to_print.object_layer->lslices.size() + 1) {
for (size_t i = 0; i + 1 < islands.size(); ++i)
if (!islands[i].by_region.empty())
island_order.emplace_back(i);
if (island_order.size() > 1) {
Points island_centroids;
island_centroids.reserve(island_order.size());
for (size_t i : island_order)
island_centroids.emplace_back(layer_to_print.object_layer->lslices[i].contour.centroid());
const Point start_near = this->last_pos();
std::vector<size_t> chain = chain_points(island_centroids, this->last_pos_defined() ? &start_near : nullptr);
std::vector<size_t> ordered;
ordered.reserve(island_order.size());
for (size_t k : chain)
ordered.emplace_back(island_order[k]);
island_order = std::move(ordered);
}
if (!islands.back().by_region.empty())
island_order.emplace_back(islands.size() - 1);
} else {
// Unexpected islands layout, keep the stored order.
for (size_t i = 0; i < islands.size(); ++i)
island_order.emplace_back(i);
}
}
for (size_t island_idx : island_order) {
ObjectByExtruder::Island &island = islands[island_idx];
const auto& by_region_specific = is_anything_overridden ? island.by_region_per_copy(by_region_per_copy_cache, static_cast<unsigned int>(instance_to_print.instance_id), extruder_id, print_wipe_extrusions != 0) : island.by_region;
// When starting a new object, use the external motion planner for the first travel move.
const Point& offset = instance_to_print.print_object.instances()[instance_to_print.instance_id].shift;
@@ -8377,29 +8518,22 @@ std::string GCode::extrusion_role_to_string_for_parser(const ExtrusionRole & rol
}
// Calculate the interpolated value for the current layer between start_value and end_value.
// Step will create equal layers steps from first to last value.
// Step > 0 splits the range into equal-width bands from first to last value (both inclusive).
// Step = 0 means gradual interpolation finishing at last value.
float GCode::interpolate_value_across_layers(float start_value, float end_value, float step) const
{
if (m_layer_index <= 1) {
return start_value;
}
else {
bool use_steps = step > 0.f;
if (use_steps) {
if (start_value > end_value) {
start_value += step;
} else {
end_value += step;
}
}
float ratio = m_layer_index / (m_layer_count - 1.f);
float value = start_value + ratio * (end_value - start_value);
if (use_steps) {
value = trunc(value / step) * step;
}
return value;
const float ratio = m_layer_index / (m_layer_count - 1.f);
if (step > 0.f) {
// Discrete equal-width bands. band is clamped to the last band so the result can't overshoot the range:
// at the top layer ratio * n_bands == n_bands, which would otherwise index one band past the end.
const int n_bands = std::lround(std::abs(end_value - start_value) / step) + 1;
const int band = std::min(n_bands - 1, static_cast<int>(ratio * n_bands));
return start_value + (end_value >= start_value ? 1.f : -1.f) * band * step;
}
return start_value + ratio * (end_value - start_value);
}
std::string encodeBase64(uint64_t value)
+34
View File
@@ -548,6 +548,40 @@ private:
// Cache for custom seam enforcers/blockers for each layer.
SeamPlacer m_seam_placer;
// One stop of the island-level tour: consecutive islands of a single instance. An instance
// can have several visits per layer when its islands are toured non-consecutively.
struct InstanceVisit
{
// Index into the per-filament InstanceToPrint vector.
size_t instance_idx;
// Islands to print, in order (indices into ObjectByExtruder::islands). Empty: print all
// islands, ordered at extrusion time.
std::vector<size_t> islands;
// First visit of this instance this layer; skirt, brim and support are emitted here.
bool first_visit;
};
// One node of the island-level tour, also used as cache key: identity plus quantized position.
struct IslandOrderNode
{
ObjectID object_id;
size_t instance_id;
// Index into ObjectByExtruder::islands, or size_t(-1) for an instance without chainable
// islands (e.g. support only), which is toured as a single stop.
size_t island_idx;
// Island centroid in G-code coordinates, quantized to 1 mm for cache stability.
Point pos;
bool operator==(const IslandOrderNode &rhs) const {
return object_id == rhs.object_id && instance_id == rhs.instance_id &&
island_idx == rhs.island_idx && pos == rhs.pos;
}
};
// Cache the per-filament island tour to avoid recomputing while the layer's island layout is
// unchanged. Key: filament_id. Value: {nodes the tour was computed from, resulting visits}.
std::map<unsigned int, std::pair<std::vector<IslandOrderNode>, std::vector<InstanceVisit>>>
m_ordering_cache;
ExtrusionQualityEstimator m_extrusion_quality_estimator;
+435
View File
@@ -0,0 +1,435 @@
// Print-object ordering strategies: implementation.
// Consolidates TSP post-processing, Snake, and Best-of-Strategies.
#include "OrderingStrategies.hpp"
#include "../Geometry.hpp"
#include "../ShortestPath.hpp"
#include <algorithm>
#include <cmath>
#include <limits>
#include <numeric>
#include <unordered_map>
#include <utility>
#include <vector>
namespace Slic3r {
/* ====================================================================
* TSP post-processing utilities
* ==================================================================== */
bool tsp_2opt_improve(std::vector<size_t>& path, const Points& centers, int max_passes)
{
size_t pn = path.size();
if (pn <= 2) return false;
// Pre-compute edge lengths once per pass to avoid redundant norm() calls.
auto recompute_edges = [&]() {
std::vector<double> el(pn);
for (size_t i = 0; i < pn; ++i) {
size_t ni = (i + 1) % pn;
el[i] = (centers[path[i]].cast<double>() - centers[path[ni]].cast<double>()).norm();
}
return el;
};
std::vector<double> el = recompute_edges();
// Pre-compute squared edge lengths for early rejection in the inner loop.
auto recompute_edges_sq = [&]() {
std::vector<double> elsq(pn);
for (size_t i = 0; i < pn; ++i) {
size_t ni = (i + 1) % pn;
elsq[i] = (centers[path[i]].cast<double>() - centers[path[ni]].cast<double>()).squaredNorm();
}
return elsq;
};
std::vector<double> elsq = recompute_edges_sq();
bool improved = false;
for (int pass = 0; max_passes <= 0 || pass < max_passes; ++pass) {
size_t best_i = pn, best_j = pn;
double best_gain = 0;
for (size_t i = 0; i < pn; ++i) {
const Vec2d& pi = centers[path[i]].cast<double>();
const Vec2d& p_in = centers[path[(i + 1) % pn]].cast<double>();
double d_i = el[i];
double d_i_sq = elsq[i];
for (size_t j = i + 2; j < pn; ++j) {
size_t j_next = (j + 1) % pn;
// Skip the swap that would reverse the entire cycle (removes both
// edges (0,1) and (pn-1,0), equivalent to traversing the cycle backwards).
if (i == 0 && j_next == 0) continue;
const Vec2d& pj = centers[path[j]].cast<double>();
const Vec2d& p_jn = centers[path[j_next]].cast<double>();
double d_j = el[j];
// Early rejection using squared distances (avoids 2 sqrt calls).
double new_a_sq = (pj - pi).squaredNorm();
double new_b_sq = (p_jn - p_in).squaredNorm();
if (new_a_sq >= d_i_sq && new_b_sq >= elsq[j]) continue;
double new_a = std::sqrt(new_a_sq);
double new_b = std::sqrt(new_b_sq);
double gain = d_i + d_j - new_a - new_b;
if (gain > best_gain) {
best_gain = gain;
best_i = i; best_j = j;
}
}
}
if (best_i == pn) break;
improved = true;
// Reverse the best swap segment
std::reverse(path.begin() + best_i + 1, path.begin() + best_j + 1);
// Recompute edge lengths after reversal
el = recompute_edges();
elsq = recompute_edges_sq();
}
return improved;
}
// Fast bounding-box overlap test (rejects most non-intersecting pairs).
static inline bool bboxes_overlap(const Point& a, const Point& b, const Point& c, const Point& d)
{
return !(std::max(a.x(), b.x()) < std::min(c.x(), d.x()) ||
std::max(c.x(), d.x()) < std::min(a.x(), b.x()) ||
std::max(a.y(), b.y()) < std::min(c.y(), d.y()) ||
std::max(c.y(), d.y()) < std::min(a.y(), b.y()));
}
bool tsp_remove_crossings(std::vector<size_t>& path, const Points& centers)
{
size_t pn = path.size();
if (pn <= 3) return false;
// Treat path as a cycle: include the closing edge (pn-1 -> 0), consistent with the other
// TSP helpers (2-opt, closing-edge rotation) that operate on the full cycle.
size_t n_edges = pn;
// Scan for first crossing; returns {i, j} or {npos, npos} if none.
auto find_crossing = [&]() -> std::pair<size_t, size_t> {
for (size_t i = 0; i < n_edges; ++i) {
const Point& ai = centers[path[i]];
const Point& bi = centers[path[(i + 1) % pn]];
for (size_t j = i + 2; j < n_edges; ++j) {
// Skip the (0, pn-1) pair: edges (0,1) and (pn-1,0) share node 0.
if (i == 0 && j == pn - 1) continue;
const Point& aj = centers[path[j]];
const Point& bj = centers[path[(j + 1) % pn]];
if (!bboxes_overlap(ai, bi, aj, bj)) continue;
if (Geometry::segments_intersect(ai, bi, aj, bj))
return {i, j};
}
}
return {std::numeric_limits<size_t>::max(), std::numeric_limits<size_t>::max()};
};
// Process crossings one at a time: find first, reverse it, restart scan.
// Cap iterations to prevent infinite loops on collinear/overlapping segments.
int max_iters = static_cast<int>(pn * pn);
bool improved = false;
while (max_iters-- > 0) {
auto [ci, cj] = find_crossing();
if (ci == std::numeric_limits<size_t>::max()) break;
improved = true;
std::reverse(path.begin() + ci + 1, path.begin() + cj + 1);
}
return improved;
}
void tsp_rotate_minimize_closing(std::vector<size_t>& path, const Points& centers)
{
size_t pn = path.size();
size_t best_start = 0;
double best_closing2 = std::numeric_limits<double>::max();
for (size_t start = 0; start < pn; ++start) {
size_t last = (start + pn - 1) % pn;
double d2 = (centers[path[start]].cast<double>() - centers[path[last]].cast<double>()).squaredNorm();
if (d2 < best_closing2) { best_closing2 = d2; best_start = start; }
}
std::rotate(path.begin(), path.begin() + best_start, path.end());
}
/* ====================================================================
* Snake ordering
* ==================================================================== */
struct SnakeRow { double avg_y; std::vector<size_t> indices; };
// --- Row threshold computation ---
// Extract unique Y values and use the median gap between them to determine
// the row threshold.
static double compute_row_threshold(const std::vector<double>& sorted_ys,
double y_min, double y_max,
size_t n,
double fraction_of_y_range,
double min_threshold_um)
{
constexpr double MIN_GAP_FILTER = 1.0; // ignore sub-micron gaps (coord_t = 1/100mm)
// Extract unique Y values
std::vector<double> unique_ys;
unique_ys.reserve(sorted_ys.size());
unique_ys.push_back(sorted_ys[0]);
for (size_t i = 1; i < sorted_ys.size(); ++i) {
if (sorted_ys[i] - sorted_ys[i - 1] > MIN_GAP_FILTER)
unique_ys.push_back(sorted_ys[i]);
}
double fallback_threshold = (y_max - y_min) * fraction_of_y_range;
if (unique_ys.size() <= 1) {
return std::max(fallback_threshold, min_threshold_um);
}
// Compute gaps between consecutive unique Y values
std::vector<double> gaps;
gaps.reserve(unique_ys.size() - 1);
for (size_t i = 1; i < unique_ys.size(); ++i)
gaps.push_back(unique_ys[i] - unique_ys[i - 1]);
if (gaps.empty()) {
return std::max(fallback_threshold, min_threshold_um);
}
// Sort gaps to find the median
std::sort(gaps.begin(), gaps.end());
double median_gap = gaps[gaps.size() / 2];
double min_gap = gaps.front();
// Threshold: half the gap between consecutive unique Y values.
double threshold = (median_gap < min_gap * 1.5) ? min_gap * 0.5 : median_gap * 0.5;
bool has_row_structure;
if (unique_ys.size() * 2 <= n) {
has_row_structure = true;
} else {
// Single-column or sparse: uniform gaps indicate a deliberate grid
double max_gap = *std::max_element(gaps.begin(), gaps.end());
has_row_structure = (max_gap < min_gap * 2.0);
}
if (has_row_structure) {
// For grid-like data, use the gap-based threshold directly.
return threshold;
}
return std::max(fallback_threshold, min_threshold_um);
}
// --- Row grouping ---
// Bin points into rows by quantising Y / threshold
static std::vector<SnakeRow> group_into_rows(const Points& centers, double row_threshold)
{
size_t n = centers.size();
std::unordered_map<int64_t, std::vector<size_t>> row_map;
for (size_t i = 0; i < n; ++i) {
int64_t y_key = static_cast<int64_t>(std::floor(static_cast<double>(centers[i].y()) / row_threshold));
row_map[y_key].push_back(i);
}
std::vector<SnakeRow> rows;
rows.reserve(row_map.size());
for (auto& [key, indices] : row_map) {
double avg_y = std::accumulate(indices.begin(), indices.end(), 0.0,
[&](double acc, size_t idx) { return acc + static_cast<double>(centers[idx].y()); })
/ indices.size();
rows.push_back({avg_y, std::move(indices)});
}
std::sort(rows.begin(), rows.end(),
[](const SnakeRow& a, const SnakeRow& b) { return a.avg_y < b.avg_y; });
return rows;
}
// Sort each row by X and greedily pick the direction (left->right or right->left)
// that minimises the transition distance from the previous row's endpoint.
static std::vector<size_t> build_serpentine_path(const Points& centers,
std::vector<SnakeRow>& rows)
{
std::vector<size_t> path;
path.reserve(centers.size());
for (size_t ri = 0; ri < rows.size(); ++ri) {
auto& row = rows[ri].indices;
std::sort(row.begin(), row.end(),
[&](size_t a, size_t b) { return centers[a].x() < centers[b].x(); });
if (ri == 0) {
path.insert(path.end(), row.begin(), row.end());
} else {
const Point& prev_end = centers[path.back()];
double dist_to_left = (prev_end.cast<double>() - centers[row.front()].cast<double>()).squaredNorm();
double dist_to_right = (prev_end.cast<double>() - centers[row.back()].cast<double>()).squaredNorm();
if (dist_to_left <= dist_to_right)
path.insert(path.end(), row.begin(), row.end());
else
path.insert(path.end(), row.rbegin(), row.rend());
}
}
return path;
}
// Row-based serpentine traversal: detect rows, bin points, snake through them.
static std::vector<size_t> row_serpentine_path(const Points& centers,
double fraction_of_y_range = 0.02,
double min_threshold_um = 1e4)
{
if (centers.empty()) return {};
size_t n = centers.size();
// Collect and sort Y coordinates.
std::vector<double> sorted_ys;
sorted_ys.reserve(n);
for (const auto& p : centers) sorted_ys.push_back(static_cast<double>(p.y()));
std::sort(sorted_ys.begin(), sorted_ys.end());
auto [ymin, ymax] = std::minmax_element(sorted_ys.begin(), sorted_ys.end());
double y_min = *ymin, y_max = *ymax;
double row_threshold = compute_row_threshold(sorted_ys, y_min, y_max, n,
fraction_of_y_range, min_threshold_um);
auto rows = group_into_rows(centers, row_threshold);
return build_serpentine_path(centers, rows);
}
std::vector<size_t> snake_core(const Points& centers)
{
if (centers.empty()) return {};
std::vector<size_t> path = row_serpentine_path(centers);
for (int iter = 0; iter < 3; ++iter) {
bool improved = tsp_2opt_improve(path, centers);
improved |= tsp_remove_crossings(path, centers);
if (!improved) break;
}
return path;
}
std::vector<const PrintInstance*> chain_print_object_instances_snake(const std::vector<const PrintObject*>& print_objects, const Point* start_near)
{
return chain_instances_with_core(print_objects, start_near, snake_core);
}
std::vector<const PrintInstance*> chain_print_object_instances_snake(const Print& print)
{
return chain_print_object_instances_snake(print.objects().vector(), nullptr);
}
/* ====================================================================
* Best-of-strategies meta-strategy
* ==================================================================== */
std::vector<const PrintInstance*> chain_print_object_instances_best_of(const std::vector<const PrintObject*>& print_objects, const Point* start_near)
{
if (print_objects.empty())
return {};
// Run all strategies.
std::vector<std::vector<const PrintInstance*>> candidates;
candidates.push_back(chain_print_object_instances(print_objects, start_near));
candidates.push_back(chain_print_object_instances_snake(print_objects, start_near));
// Compute metrics for each candidate.
struct Candidate { double total_len; double max_edge; };
std::vector<Candidate> metrics;
metrics.reserve(candidates.size());
for (size_t i = 0; i < candidates.size(); ++i) {
double total = 0.0;
double mx = 0.0;
for (size_t j = 0; j < candidates[i].size(); ++j) {
size_t k = (j + 1) % candidates[i].size();
double d = (candidates[i][j]->shift.cast<double>() - candidates[i][k]->shift.cast<double>()).norm();
total += d;
if (d > mx) mx = d;
}
metrics.push_back({total, mx});
}
// Pick shortest total path; tiebreak on smallest max edge.
auto best_it = std::min_element(metrics.begin(), metrics.end(),
[](const Candidate& a, const Candidate& b) {
return a.total_len < b.total_len ||
(a.total_len == b.total_len && a.max_edge < b.max_edge);
});
size_t best = static_cast<size_t>(std::distance(metrics.begin(), best_it));
return candidates[best];
}
std::vector<const PrintInstance*> chain_print_object_instances_best_of(const Print& print)
{
return chain_print_object_instances_best_of(print.objects().vector(), nullptr);
}
/* ====================================================================
* Island-level ordering entry point
* ==================================================================== */
std::vector<size_t> order_points_with_strategy(const Points& points, PrintOrder print_order, const Point* start_near)
{
if (points.empty())
return {};
if (print_order != PrintOrder::Snake && print_order != PrintOrder::BestOfStrategies)
// Nearest neighbor + post-processing; honours start_near natively.
return chain_points_with_postprocessing(points, start_near);
auto run_snake = [&points, start_near]() {
std::vector<size_t> path = snake_core(points);
if (start_near != nullptr && !path.empty()) {
// Start the cycle at the point closest to start_near.
size_t best_start = 0;
double best_d2 = std::numeric_limits<double>::max();
for (size_t k = 0; k < points.size(); ++k) {
double d2 = (points[k].cast<double>() - start_near->cast<double>()).squaredNorm();
if (d2 < best_d2) { best_d2 = d2; best_start = k; }
}
auto it = std::find(path.begin(), path.end(), best_start);
if (it != path.begin() && it != path.end())
std::rotate(path.begin(), it, path.end());
} else {
tsp_rotate_minimize_closing(path, points);
}
return path;
};
if (print_order == PrintOrder::Snake)
return run_snake();
// Best-of: pick the shortest total cycle; tiebreak on smallest max edge.
std::vector<std::vector<size_t>> candidates;
candidates.emplace_back(chain_points_with_postprocessing(points, start_near));
candidates.emplace_back(run_snake());
size_t best = 0;
double best_len = std::numeric_limits<double>::max();
double best_edge = std::numeric_limits<double>::max();
for (size_t i = 0; i < candidates.size(); ++i) {
double len = tsp_cycle_path_length(candidates[i], points);
double edge = tsp_max_edge_length(candidates[i], points);
if (len < best_len || (len == best_len && edge < best_edge)) {
best_len = len; best_edge = edge; best = i;
}
}
return candidates[best];
}
} // namespace Slic3r
+148
View File
@@ -0,0 +1,148 @@
// Print-object ordering strategies and shared TSP post-processing utilities.
#ifndef slic3r_OrderingStrategies_hpp_
#define slic3r_OrderingStrategies_hpp_
#include "../libslic3r.h"
#include "../Point.hpp"
#ifndef SLIC3R_TEST_HARNESS
#include "../Print.hpp"
#endif
#include <algorithm>
#include <limits>
#include <utility>
#include <vector>
namespace Slic3r {
// --- Path improvement (operate on index vectors into `centers`) ---
// 2-opt improvement: reverses segments that reduce total cycle path length.
// Returns true if any improvement was made.
bool tsp_2opt_improve(std::vector<size_t>& path, const Points& centers, int max_passes = 10);
// Crossing removal: reverse any segment pair whose edges geometrically cross.
// Returns true if any crossing was removed.
bool tsp_remove_crossings(std::vector<size_t>& path, const Points& centers);
// Rotate the cycle so the closing edge (last -> first) is minimized.
void tsp_rotate_minimize_closing(std::vector<size_t>& path, const Points& centers);
// Total Euclidean path length of a cycle (including closing edge).
inline double tsp_cycle_path_length(const std::vector<size_t>& path, const Points& centers)
{
if (path.size() < 2) return 0.0;
double total = 0.0;
for (size_t i = 0; i < path.size(); ++i) {
size_t next = (i + 1) % path.size();
total += (centers[path[i]].cast<double>() - centers[path[next]].cast<double>()).norm();
}
return total;
}
// Maximum edge length of a cycle (including closing edge).
inline double tsp_max_edge_length(const std::vector<size_t>& path, const Points& centers)
{
if (path.size() < 2) return 0.0;
double mx = 0.0;
for (size_t i = 0; i < path.size(); ++i) {
size_t next = (i + 1) % path.size();
double d = (centers[path[i]].cast<double>() - centers[path[next]].cast<double>()).norm();
if (d > mx) mx = d;
}
return mx;
}
#ifndef SLIC3R_TEST_HARNESS
// --- Wrapper boilerplate ---
// Collect instance centers from PrintObjects, optionally pre-rotate to honour
// start_near, call a core algorithm, and map the result back to PrintInstance*.
template<typename CoreFn>
std::vector<const PrintInstance*> chain_instances_with_core(
const std::vector<const PrintObject*>& print_objects,
const Point* start_near,
CoreFn&& core_fn)
{
Points instance_centers;
std::vector<std::pair<size_t, size_t>> instances;
for (size_t i = 0; i < print_objects.size(); ++i) {
const PrintObject& object = *print_objects[i];
for (size_t j = 0; j < object.instances().size(); ++j) {
instance_centers.emplace_back(object.instances()[j].shift);
instances.emplace_back(i, j);
}
}
if (instance_centers.empty()) return {};
// If start_near is provided, pre-rotate so closest point is first.
if (start_near != nullptr) {
size_t best_start = 0;
double best_d2 = std::numeric_limits<double>::max();
for (size_t k = 0; k < instance_centers.size(); ++k) {
double d2 = (instance_centers[k].cast<double>() - start_near->cast<double>()).squaredNorm();
if (d2 < best_d2) { best_d2 = d2; best_start = k; }
}
std::rotate(instance_centers.begin(), instance_centers.begin() + best_start, instance_centers.end());
std::rotate(instances.begin(), instances.begin() + best_start, instances.end());
}
auto path = core_fn(instance_centers);
// Rotate the cycle so the first element is the best starting point.
// When start_near is provided, pick the point closest to it (preserving
// the pre-rotation). Otherwise minimise the closing edge.
if (start_near != nullptr && !path.empty()) {
// Pre-rotation already put the closest point at index 0.
// Find where index 0 appears in the path and rotate it to the front.
auto it = std::find(path.begin(), path.end(), size_t(0));
if (it != path.begin())
std::rotate(path.begin(), it, path.end());
} else {
tsp_rotate_minimize_closing(path, instance_centers);
}
std::vector<const PrintInstance*> out;
out.reserve(path.size());
for (size_t step : path) {
out.emplace_back(&print_objects[instances[step].first]->instances()[instances[step].second]);
}
return out;
}
#endif // SLIC3R_TEST_HARNESS
// --- Core algorithms (operate on raw Points, return index permutations) ---
// Snake ordering: row grouping + serpentine traversal + post-processing.
std::vector<size_t> snake_core(const Points& centers);
#ifndef SLIC3R_TEST_HARNESS
// --- Production wrappers ---
// Snake ordering.
std::vector<const PrintInstance*> chain_print_object_instances_snake(const std::vector<const PrintObject*>& print_objects, const Point* start_near);
std::vector<const PrintInstance*> chain_print_object_instances_snake(const Print& print);
// Best-of-strategies: run all strategies and return the shortest result.
// Primary: shortest total path; secondary tiebreaker: smallest max edge.
std::vector<const PrintInstance*> chain_print_object_instances_best_of(const std::vector<const PrintObject*>& print_objects, const Point* start_near);
std::vector<const PrintInstance*> chain_print_object_instances_best_of(const Print& print);
// Order raw points with the selected strategy, returning an index permutation. Island-level
// counterpart of the chain_print_object_instances_* helpers. The returned cycle starts at the
// point closest to start_near; orders without a dedicated strategy use nearest-neighbor chaining.
std::vector<size_t> order_points_with_strategy(const Points& points, PrintOrder print_order, const Point* start_near);
#endif // SLIC3R_TEST_HARNESS
} // namespace Slic3r
#endif /* slic3r_OrderingStrategies_hpp_ */
+266 -34
View File
@@ -360,6 +360,14 @@ static ClipperLib_Z::Paths clip_extrusion(const ClipperLib_Z::Path& subject, con
return clipped_paths;
}
static double clipper_z_path_length(const ClipperLib_Z::Path &path)
{
double len = 0.;
for (size_t i = 1; i < path.size(); ++ i)
len += (Vec2d(double(path[i].x()), double(path[i].y())) - Vec2d(double(path[i - 1].x()), double(path[i - 1].y()))).norm();
return len;
}
struct PerimeterGeneratorArachneExtrusion
{
Arachne::ExtrusionLine* extrusion = nullptr;
@@ -571,17 +579,156 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p
return extrusion_coll;
}
// ORCA: only_one_wall_top detects the top as "slice − upper", so a feature rising from the middle of a
// top surface becomes an enclosed hole that gets ringed with extra inner walls. Fill those holes back
// into the top. Only holes that are both covered by the upper layer (excludes bridges) and backed by
// solid material (excludes voids) are filled.
static ExPolygons fill_enclosed_top_feature_holes(const ExPolygons &top, const Polygons &covered_by_upper, const ExPolygons &solid)
// ORCA: only_one_wall_top acts on top surfaces, so without a top shell there is nothing for it to act on: zero top
// shell layers retype the top surfaces as internal, see LayerRegion::prepare_fill_surfaces(). A 0% top surface
// density does leave a top surface - just an unfilled one - so it does not disable the feature.
// ConfigManipulation::toggle_print_fff_options() hides the option under the same condition, so a profile that left
// it enabled does not act behind a hidden checkbox.
static bool has_top_shell_layers(const PrintRegionConfig &config)
{
ExPolygons filled = top;
for (ExPolygon &ex : filled)
ex.holes.clear();
const ExPolygons feature_holes = intersection_ex(intersection_ex(diff_ex(filled, top), covered_by_upper), solid);
return feature_holes.empty() ? top : union_ex(top, feature_holes);
return config.top_shell_layers.value > 0;
}
// ORCA: only_one_wall_first_layer thins the first layer to a single wall, the bottom counterpart of the above and
// gated the same way: zero bottom shell layers retype the bottom surfaces as internal, so that wall would ring
// sparse infill on the bed. The bottom surface density plays no part - an unfilled bottom surface is still a bottom
// surface, exactly as for the top - and it cannot reach zero anyway, being capped at a 10% minimum.
static bool has_bottom_shell_layers(const PrintRegionConfig &config)
{
return config.bottom_shell_layers.value > 0;
}
// ORCA: the inner walls are only given up when a top fill takes their space, and it has to actually reach it -
// a 0% top surface density leaves no fill at all, and without top_surface_expansion the fill never grows over
// them. Either way the original generation is kept (re-onion the not-top region), which is what users of
// only_one_wall_top alone have always got.
static bool top_fill_replaces_inner_walls(const PrintRegionConfig &config)
{
return has_top_shell_layers(config) && config.top_surface_density.value > 0 && config.top_surface_expansion.value > 0;
}
// ORCA: only_one_wall_top - cheap per-vertex classification of a wall against the top surface. Only Partial
// needs the geometry clipped or measured; a segment crossing the top with no vertex inside is rare enough to ignore.
enum class TopOverlap { None, Partial, Full };
static bool point_over_top(const Point &p, const ExPolygons &top_region, const BoundingBox &top_region_bbox)
{
if (! top_region_bbox.contains(p))
return false;
for (const ExPolygon &ex : top_region)
if (ex.contains(p, false))
return true;
return false;
}
static TopOverlap classify_over_top(const Points &pts, const ExPolygons &top_region, const BoundingBox &top_region_bbox)
{
size_t inside = 0;
for (const Point &p : pts)
if (point_over_top(p, top_region, top_region_bbox))
++ inside;
return inside == 0 ? TopOverlap::None : inside == pts.size() ? TopOverlap::Full : TopOverlap::Partial;
}
static TopOverlap classify_over_top(const Arachne::ExtrusionLine &el, const ExPolygons &top_region, const BoundingBox &top_region_bbox)
{
size_t inside = 0;
for (const Arachne::ExtrusionJunction &j : el.junctions)
if (point_over_top(j.p, top_region, top_region_bbox))
++ inside;
return inside == 0 ? TopOverlap::None : inside == el.junctions.size() ? TopOverlap::Full : TopOverlap::Partial;
}
// ORCA: only_one_wall_top for Arachne - cut out of the already generated inner walls the parts running over the top
// surface, so geometry that continues upward keeps its walls. A wall too short over the top to be worth slitting open
// is left whole, its footprint reported in kept_over_top for the caller to withhold from the top fill.
static void clip_inner_walls_over_top(std::vector<Arachne::VariableWidthLines> &inner_perimeters, const ExPolygons &top_region, coord_t perimeter_width, Polygons &kept_over_top)
{
const BoundingBox top_region_bbox = get_extents(top_region).inflated(SCALED_EPSILON);
auto covered_by = [](const Arachne::ExtrusionLine &el) {
Polyline centerline;
centerline.points.reserve(el.junctions.size());
coord_t width = 0;
for (const Arachne::ExtrusionJunction &j : el.junctions) {
centerline.points.emplace_back(j.p);
width = std::max(width, j.w);
}
return offset(centerline, float(width) / 2.f);
};
// Pull the cut back by half a wall width: the clip severs the centerline, but the bead's rounded end
// extends half a width past its endpoint and would otherwise overlap the top fill.
ClipperLib_Z::Paths top_paths_z;
for (const Polygon &poly : to_polygons(offset_ex(top_region, float(perimeter_width) / 2.f))) {
top_paths_z.emplace_back();
ClipperLib_Z::Path &out = top_paths_z.back();
out.reserve(poly.points.size());
for (const Point &pt : poly.points)
out.emplace_back(pt.x(), pt.y(), 0);
}
for (Arachne::VariableWidthLines &inner_perimeter : inner_perimeters) {
Arachne::VariableWidthLines kept;
kept.reserve(inner_perimeter.size());
for (Arachne::ExtrusionLine &el : inner_perimeter) {
if (el.empty())
continue;
const TopOverlap overlap = classify_over_top(el, top_region, top_region_bbox);
if (overlap == TopOverlap::None) {
kept.emplace_back(std::move(el));
continue;
}
if (overlap == TopOverlap::Full)
continue; // the clip below would return nothing anyway
ClipperLib_Z::Path subject;
subject.reserve(el.size());
for (const Arachne::ExtrusionJunction &j : el.junctions)
subject.emplace_back(j.p.x(), j.p.y(), j.w);
ClipperLib_Z::Paths pieces = clip_extrusion(subject, top_paths_z, ClipperLib_Z::ctDifference);
// Clipper treats the subject as an open polyline, so it also cuts a closed loop at its (arbitrary)
// start vertex and may reverse pieces. Stitch pieces sharing an endpoint back together.
auto same_pt = [](const ClipperLib_Z::IntPoint &p, const ClipperLib_Z::IntPoint &q) {
return std::abs(p.x() - q.x()) <= SCALED_EPSILON && std::abs(p.y() - q.y()) <= SCALED_EPSILON;
};
for (size_t i = 0; i < pieces.size(); ++ i) {
for (size_t j = i + 1; j < pieces.size();) {
ClipperLib_Z::Path &a = pieces[i];
ClipperLib_Z::Path &b = pieces[j];
if (same_pt(a.front(), b.front()) || same_pt(a.front(), b.back()))
std::reverse(a.begin(), a.end());
if (same_pt(a.back(), b.back()))
std::reverse(b.begin(), b.end());
if (same_pt(a.back(), b.front())) {
a.insert(a.end(), b.begin() + 1, b.end());
pieces.erase(pieces.begin() + j);
j = i + 1; // the merged path has new endpoints, restart the scan
} else
++ j;
}
}
// If the clip removed next to nothing, keep the loop untouched instead of slitting it open. The
// half-width pull-back above already costs about one width per crossing, hence two widths.
double kept_length = 0.;
for (const ClipperLib_Z::Path &path : pieces)
kept_length += clipper_z_path_length(path);
if (clipper_z_path_length(subject) - kept_length < 2. * double(perimeter_width)) {
append(kept_over_top, covered_by(el));
kept.emplace_back(std::move(el));
continue;
}
for (const ClipperLib_Z::Path &path : pieces) {
Arachne::ExtrusionLine clipped(el.inset_idx, el.is_odd);
clipped.junctions.reserve(path.size());
for (const ClipperLib_Z::IntPoint &pt : path)
clipped.junctions.emplace_back(Point(pt.x(), pt.y()), coord_t(pt.z()), el.inset_idx);
// Discard tiny leftovers that would print as zits.
if (clipped.size() >= 2 && clipped.getLength() >= perimeter_width)
kept.emplace_back(std::move(clipped));
}
}
inner_perimeter = std::move(kept);
}
}
void PerimeterGenerator::split_top_surfaces(const ExPolygons &orig_polygons, ExPolygons &top_fills,
@@ -649,7 +796,6 @@ void PerimeterGenerator::split_top_surfaces(const ExPolygons &orig_polygons, ExP
ExPolygons delete_bridge = diff_ex(orig_polygons, bridge_checker, ApplySafetyOffset::Yes);
ExPolygons top_polygons = diff_ex(delete_bridge, upper_polygons_series_clipped, ApplySafetyOffset::Yes);
top_polygons = fill_enclosed_top_feature_holes(top_polygons, upper_polygons_series_clipped, orig_polygons);
// get the not-top surface, from the "real top" but enlarged by external_infill_margin (and the
// min_width_top_surface we removed a bit before)
@@ -1234,6 +1380,11 @@ void PerimeterGenerator::process_classic()
for (const Surface &surface : all_surfaces)
surface_exp.push_back(surface.expolygon);
std::vector<size_t> surface_order = chain_expolygons(surface_exp);
// ORCA: neither one-wall option has a surface to act on without the shell behind it, see
// has_top_shell_layers() / has_bottom_shell_layers(). Gated here so every use below - including the
// topmost and first layers - sees the same answer.
const bool only_one_wall_top = this->config->only_one_wall_top && has_top_shell_layers(*this->config);
const bool only_one_wall_first_layer = this->config->only_one_wall_first_layer && has_bottom_shell_layers(*this->config);
for (size_t order_idx = 0; order_idx < surface_order.size(); order_idx++) {
const Surface &surface = all_surfaces[surface_order[order_idx]];
// detect how many perimeters must be generated for this island
@@ -1241,16 +1392,23 @@ void PerimeterGenerator::process_classic()
int sparse_infill_density = this->config->sparse_infill_density.value;
if (this->config->alternate_extra_wall && this->layer_id % 2 == 1 && !m_spiral_vase && sparse_infill_density > 0) // add alternating extra wall
loop_number++;
if (this->layer_id == object_config->raft_layers && this->config->only_one_wall_first_layer)
if (this->layer_id == object_config->raft_layers && only_one_wall_first_layer)
loop_number = 0;
// Set the topmost layer to be one wall
if (loop_number > 0 && config->only_one_wall_top && this->upper_slices == nullptr)
if (loop_number > 0 && only_one_wall_top && this->upper_slices == nullptr)
loop_number = 0;
ExPolygons last = union_ex(surface.expolygon.simplify_p(surface_simplify_resolution));
ExPolygons gaps;
ExPolygons top_fills;
ExPolygons fill_clip;
// ORCA: only_one_wall_top, all empty unless this island has a top surface on this layer. See the
// post-onion reduction below: the region to keep clear of inner walls, the space freed by the dropped
// walls (goes to infill, not left as a void) and the space held by the kept ones (withheld from the fill).
ExPolygons one_wall_top_region;
ExPolygons one_wall_top_reclaimed;
Polygons one_wall_top_kept_bands;
bool apply_one_wall_top = false;
if (loop_number >= 0) {
// In case no perimeters are to be generated, loop_number will equal to -1.
std::vector<PerimeterGeneratorLoops> contours(loop_number+1); // depth => loops
@@ -1389,8 +1547,19 @@ void PerimeterGenerator::process_classic()
//BBS: refer to superslicer
//store surface for top infill if only_one_wall_top
if (i == 0 && i!=loop_number && config->only_one_wall_top && !surface.is_bridge() && this->upper_slices != NULL) {
this->split_top_surfaces(last, top_fills, last, fill_clip);
if (i == 0 && i!=loop_number && only_one_wall_top && !surface.is_bridge() && this->upper_slices != NULL) {
if (top_fill_replaces_inner_walls(*this->config)) {
// ORCA: take the top fill and the keep-out region but leave `last` as the real geometry,
// so the onion follows it and the walls over the top are reduced in one step below.
ExPolygons non_top_polygons;
this->split_top_surfaces(last, top_fills, non_top_polygons, fill_clip);
apply_one_wall_top = !top_fills.empty();
if (apply_one_wall_top)
one_wall_top_region = diff_ex(last, non_top_polygons);
} else {
// Onion the not-top region only, so the remaining walls stop at the top boundary.
this->split_top_surfaces(last, top_fills, last, fill_clip);
}
}
if (i == loop_number && (! has_gap_fill || this->config->sparse_infill_density.value == 0)) {
@@ -1400,6 +1569,46 @@ void PerimeterGenerator::process_classic()
}
}
// ORCA: only_one_wall_top reduction - drop the inner walls (depth > 0) running over the top surface and
// take that space back from the gaps, leaving the top with the outer wall and the top infill. Classic
// perimeters are closed loops, so a wall can only be kept or dropped whole; one that merely grazes the
// top (same tolerance as the Arachne clip) is kept and withheld from the top fill instead.
if (apply_one_wall_top) {
const BoundingBox top_region_bbox = get_extents(one_wall_top_region).inflated(SCALED_EPSILON);
const double grazing_tolerance = 2. * double(perimeter_width);
// The band a wall covers, taken around its centerline so the orientation of holes does not matter.
auto wall_band = [perimeter_spacing](const Polygon &poly) {
Polygon centerline = poly;
centerline.make_counter_clockwise();
return diff(offset(centerline, float(perimeter_spacing) / 2.f),
offset(centerline, -float(perimeter_spacing) / 2.f));
};
Polygons dropped_wall_bands;
auto reduce_over_top = [&](PerimeterGeneratorLoops &loops) {
loops.erase(std::remove_if(loops.begin(), loops.end(), [&](const PerimeterGeneratorLoop &loop) {
const TopOverlap overlap = classify_over_top(loop.polygon.points, one_wall_top_region, top_region_bbox);
if (overlap == TopOverlap::None)
return false;
// Only a wall straddling the boundary is worth measuring; a wall wholly over the top goes.
if (overlap == TopOverlap::Partial &&
total_length(intersection_pl(Polylines{ loop.polygon.split_at_first_point() }, one_wall_top_region)) < grazing_tolerance) {
append(one_wall_top_kept_bands, wall_band(loop.polygon));
return false;
}
append(dropped_wall_bands, wall_band(loop.polygon));
return true;
}), loops.end());
};
for (int d = 1; d <= loop_number; ++ d) {
reduce_over_top(contours[d]);
reduce_over_top(holes[d]);
}
if (! gaps.empty())
gaps = diff_ex(gaps, one_wall_top_region);
if (! dropped_wall_bands.empty())
one_wall_top_reclaimed = diff_ex(dropped_wall_bands, one_wall_top_region);
}
// nest loops: holes first
for (int d = 0; d <= loop_number; ++ d) {
PerimeterGeneratorLoops &holes_d = holes[d];
@@ -1634,7 +1843,10 @@ void PerimeterGenerator::process_classic()
and use zigzag). */
//FIXME Vojtech: This grows by a rounded extrusion width, not by line spacing,
// therefore it may cover the area, but no the volume.
last = diff_ex(last, gap_fill.polygons_covered_by_width(10.f));
Polygons gap_fill_covered = gap_fill.polygons_covered_by_width(10.f);
last = diff_ex(last, gap_fill_covered);
if (! one_wall_top_reclaimed.empty())
one_wall_top_reclaimed = diff_ex(one_wall_top_reclaimed, gap_fill_covered);
this->gap_fill->append(std::move(gap_fill.entities));
}
@@ -1679,9 +1891,15 @@ void PerimeterGenerator::process_classic()
// append infill areas to fill_surfaces
//if any top_fills, grow them by ext_perimeter_spacing/2 to have the real un-anchored fill
ExPolygons top_infill_exp = intersection_ex(fill_clip, offset_ex(top_fills, double(ext_perimeter_spacing / 2)));
// ORCA: only_one_wall_top - route the top fill around the walls kept despite grazing the top.
if (!one_wall_top_kept_bands.empty())
top_infill_exp = diff_ex(top_infill_exp, one_wall_top_kept_bands);
if (!top_fills.empty()) {
infill_exp = union_ex(infill_exp, offset_ex(top_infill_exp, double(top_infill_peri_overlap)));
}
// ORCA: only_one_wall_top - what the top fill does not cover of the dropped walls goes to infill.
if (!one_wall_top_reclaimed.empty())
infill_exp = union_ex(infill_exp, one_wall_top_reclaimed);
this->fill_surfaces->append(infill_exp, stInternal);
apply_extra_perimeters(infill_exp);
@@ -1700,6 +1918,8 @@ void PerimeterGenerator::process_classic()
double(-inset - infill_peri_overlap));
if (!top_fills.empty())
polyWithoutOverlap = union_ex(polyWithoutOverlap, top_infill_exp);
if (!one_wall_top_reclaimed.empty())
polyWithoutOverlap = union_ex(polyWithoutOverlap, one_wall_top_reclaimed);
this->fill_no_overlap->insert(this->fill_no_overlap->end(), polyWithoutOverlap.begin(), polyWithoutOverlap.end());
}
@@ -2138,6 +2358,11 @@ void PerimeterGenerator::process_arachne()
process_no_bridge(all_surfaces, perimeter_spacing, ext_perimeter_width);
// BBS: don't simplify too much which influence arc fitting when export gcode if arc_fitting is enabled
double surface_simplify_resolution = (print_config->enable_arc_fitting && !this->has_fuzzy_skin) ? 0.2 * m_scaled_resolution : m_scaled_resolution;
// ORCA: neither one-wall option has a surface to act on without the shell behind it, see
// has_top_shell_layers() / has_bottom_shell_layers(). Gated here so every use below - including the
// topmost and first layers - sees the same answer.
const bool only_one_wall_top = this->config->only_one_wall_top && has_top_shell_layers(*this->config);
const bool only_one_wall_first_layer = this->config->only_one_wall_first_layer && has_bottom_shell_layers(*this->config);
// we need to process each island separately because we might have different
// extra perimeters for each one
for (const Surface& surface : all_surfaces) {
@@ -2150,12 +2375,12 @@ void PerimeterGenerator::process_arachne()
// Set the bottommost layer to be one wall
const bool is_bottom_layer = (this->layer_id == object_config->raft_layers) ? true : false;
if (is_bottom_layer && this->config->only_one_wall_first_layer)
if (is_bottom_layer && only_one_wall_first_layer)
loop_number = 0;
// Orca: set the topmost layer to be one wall according to the config
const bool is_topmost_layer = (this->upper_slices == nullptr) ? true : false;
if (is_topmost_layer && loop_number > 0 && config->only_one_wall_top)
if (is_topmost_layer && loop_number > 0 && only_one_wall_top)
loop_number = 0;
auto apply_precise_outer_wall = config->precise_outer_wall && config->wall_sequence == WallSequence::InnerOuter;
@@ -2175,10 +2400,10 @@ void PerimeterGenerator::process_arachne()
//PS: One wall top surface for Arachne
ExPolygons top_expolygons;
// Calculate how many inner loops remain when TopSurfaces is selected.
const int inner_loop_number = (config->only_one_wall_top && upper_slices != nullptr) ? loop_number - 1 : -1;
const int inner_loop_number = (only_one_wall_top && upper_slices != nullptr) ? loop_number - 1 : -1;
// Set one perimeter when TopSurfaces is selected.
if (config->only_one_wall_top && loop_number > 0)
if (only_one_wall_top && loop_number > 0)
loop_number = 0;
Arachne::WallToolPathsParams input_params_tmp = input_params;
@@ -2209,7 +2434,6 @@ void PerimeterGenerator::process_arachne()
upper_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*upper_slices, infill_contour_bbox);
top_expolygons = diff_ex(infill_contour, upper_slices_clipped);
top_expolygons = fill_enclosed_top_feature_holes(top_expolygons, upper_slices_clipped, infill_contour);
if (!top_expolygons.empty()) {
if (lower_slices != nullptr) {
@@ -2230,25 +2454,33 @@ void PerimeterGenerator::process_arachne()
// due to thin lines being generated
top_expolygons = offset2_ex(top_expolygons, -top_surface_min_width, top_surface_min_width + float(perimeter_width * 0.85));
// Get the not-top ExPolygons (including bridges) from current slices and expanded real top ExPolygons (without bridges).
const ExPolygons not_top_expolygons = diff_ex(infill_contour, top_expolygons);
// Get final top ExPolygons.
// Get final top ExPolygons (bridges were excluded above, so they stay walled).
top_expolygons = intersection_ex(top_expolygons, infill_contour);
const Polygons not_top_polygons = to_polygons(offset_ex(not_top_expolygons,wall_0_inset));
Arachne::WallToolPaths inner_wall_tool_paths(not_top_polygons, perimeter_spacing, perimeter_spacing, coord_t(inner_loop_number + 1), 0, layer_height, input_params_tmp);
// ORCA: onion the real region (inside the outer wall) so the remaining walls follow the actual
// geometry, then cut away the parts over the top surface. Re-onioning the non-top complement
// instead - the fallback when there is no top fill - walls the top/non-top interface and rings
// top-surface islands with inner walls that don't exist when the feature is disabled.
const bool clip_walls_over_top = top_fill_replaces_inner_walls(*this->config);
const Polygons inner_region = to_polygons(offset_ex(clip_walls_over_top ? infill_contour
: diff_ex(infill_contour, top_expolygons),
wall_0_inset));
Arachne::WallToolPaths inner_wall_tool_paths(inner_region, perimeter_spacing, perimeter_spacing, coord_t(inner_loop_number + 1), 0, layer_height, input_params_tmp);
std::vector<Arachne::VariableWidthLines> inner_perimeters = inner_wall_tool_paths.getToolPaths();
// Recalculate indexes of inner perimeters before merging them.
if (!perimeters.empty()) {
for (Arachne::VariableWidthLines &inner_perimeter : inner_perimeters) {
if (inner_perimeter.empty())
continue;
if (clip_walls_over_top) {
Polygons kept_over_top;
clip_inner_walls_over_top(inner_perimeters, top_expolygons, perimeter_width, kept_over_top);
// Route the top fill around the walls kept despite grazing the top.
if (! kept_over_top.empty())
top_expolygons = diff_ex(top_expolygons, kept_over_top);
}
// Recalculate indexes of inner perimeters before merging them: they come after the single outer wall.
if (!perimeters.empty())
for (Arachne::VariableWidthLines &inner_perimeter : inner_perimeters)
for (Arachne::ExtrusionLine &el : inner_perimeter)
++el.inset_idx;
}
}
perimeters.insert(perimeters.end(), inner_perimeters.begin(), inner_perimeters.end());
infill_contour = union_ex(top_expolygons, inner_wall_tool_paths.getInnerContour());
+22 -1
View File
@@ -332,6 +332,8 @@ CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(PrintSequence)
static t_config_enum_values s_keys_map_PrintOrder{
{ "default", int(PrintOrder::Default) },
{ "as_obj_list", int(PrintOrder::AsObjectList)},
{ "best_of", int(PrintOrder::BestOfStrategies)},
{ "snake", int(PrintOrder::Snake)},
};
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(PrintOrder)
@@ -2018,12 +2020,30 @@ void PrintConfigDef::init_fff_params()
def = this->add("print_order", coEnum);
def->label = L("Intra-layer order");
def->tooltip = L("Print order within a single layer.");
def->tooltip = L("Order in which object instances are visited within a single layer, which controls how much "
"travel is spent moving between them.\n\n"
"Default: nearest-neighbor chaining, refined with 2-opt and crossing removal. A good general "
"choice.\n"
"As object list: instances are printed in the same order as the object list, without any path "
"optimization. Use it when you need a predictable, manually controlled order.\n"
"Best of all (shortest path): every strategy is evaluated and the shortest one is used. The "
"object instance order is decided once for the whole print, while the ordering of individual "
"islands is decided per layer, so different layers may end up using different strategies. "
"Slightly slower to slice.\n"
"Snake: serpentine row-by-row traversal, refined with 2-opt. Well suited to regular grids of "
"many small parts.\n\n"
"With multiple filaments or tools in the same layer, minimizing tool changes takes priority: "
"objects are grouped by filament first and this setting only orders the instances within each "
"filament group, so the overall sequence may not look like the shortest path across the plate.");
def->enum_keys_map = &ConfigOptionEnum<PrintOrder>::get_enum_values();
def->enum_values.push_back("default");
def->enum_values.push_back("as_obj_list");
def->enum_values.push_back("best_of");
def->enum_values.push_back("snake");
def->enum_labels.push_back(L("Default"));
def->enum_labels.push_back(L("As object list"));
def->enum_labels.push_back(L("Best of all (shortest path)"));
def->enum_labels.push_back(L("Snake"));
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionEnum<PrintOrder>(PrintOrder::Default));
@@ -5648,6 +5668,7 @@ void PrintConfigDef::init_fff_params()
// Orca:
def = this->add("retract_after_wipe", coPercents);
def->label = L("Retract amount after wipe");
// xgettext:no-c-format, no-boost-format
def->tooltip = L("The length of fast retraction after wipe, relative to retraction length.\n"
"The value will be clamped by 100% minus the retract amount before the wipe value.");
def->sidetext = "%";
+2
View File
@@ -234,6 +234,8 @@ enum class PrintOrder
{
Default,
AsObjectList,
BestOfStrategies, // run all custom strategies, pick the shortest total path
Snake, // snake-like row traversal (back-and-forth) + 2-opt
Count,
};
+49 -34
View File
@@ -1364,7 +1364,6 @@ bool PrintObject::invalidate_state_by_config_options(
|| opt_key == "infill_combination_max_layer_height"
|| opt_key == "bottom_shell_thickness"
|| opt_key == "top_shell_thickness"
|| opt_key == "top_surface_expansion"
|| opt_key == "top_surface_expansion_margin"
|| opt_key == "top_surface_expansion_direction"
|| opt_key == "minimum_sparse_infill_area"
@@ -1400,7 +1399,6 @@ bool PrintObject::invalidate_state_by_config_options(
|| opt_key == "infill_anchor"
|| opt_key == "infill_anchor_max"
|| opt_key == "top_surface_line_width"
|| opt_key == "top_surface_density"
|| opt_key == "bottom_surface_density"
|| opt_key == "center_of_surface_pattern"
|| opt_key == "separated_infills"
@@ -1434,6 +1432,24 @@ bool PrintObject::invalidate_state_by_config_options(
is_approx(new_density->value, 0.) || is_approx(new_density->value, 100.))
steps.emplace_back(posPerimeters);
steps.emplace_back(posPrepareInfill);
} else if (opt_key == "top_surface_density") {
// ORCA: 0% means no top solid fill, which switches off both the top surface expansion and the wall
// removal over top surfaces. Only crossing zero matters; posPerimeters cascades to posPrepareInfill.
const auto *old_density = old_config.option<ConfigOptionPercent>(opt_key);
const auto *new_density = new_config.option<ConfigOptionPercent>(opt_key);
assert(old_density && new_density);
if (is_approx(old_density->value, 0.) || is_approx(new_density->value, 0.))
steps.emplace_back(posPerimeters);
steps.emplace_back(posInfill);
} else if (opt_key == "top_surface_expansion") {
// ORCA: without the expansion the top fill never reaches the space freed by only_one_wall_top, so the
// walls over top surfaces are kept. Only crossing zero matters; posPerimeters cascades to posPrepareInfill.
const auto *old_expansion = old_config.option<ConfigOptionFloat>(opt_key);
const auto *new_expansion = new_config.option<ConfigOptionFloat>(opt_key);
assert(old_expansion && new_expansion);
if (old_expansion->value <= 0. || new_expansion->value <= 0.)
steps.emplace_back(posPerimeters);
steps.emplace_back(posPrepareInfill);
} else if (opt_key == "internal_solid_infill_line_width") {
// This value is used for calculating perimeter - infill overlap, thus perimeters need to be recalculated.
steps.emplace_back(posPerimeters);
@@ -1760,51 +1776,50 @@ void PrintObject::detect_surfaces_type()
}
}
// ORCA: Expand the top surfaces outward by top_surface_expansion in every direction. This
// enlarges the top solid infill and, in particular, grows it over the covered material left
// by features rising from the middle of a top surface (filling holes and joining tops so the
// features rest on it). The expansion stays inside the section it belongs to: each connected
// solid island has its own outer wall, so the top is grown within each island separately and
// clipped to it - growing one island's top across the gap into another island (which may have
// no top surface, leaving a partially filled layer) is never allowed. The top infill sits
// inside the perimeters, so the margin is measured from the walls: the island is inset by the
// band the walls consume (outer wall + inner walls) plus the configured margin, making that
// value the real clearance between the expanded top and the walls (avoiding a hull line). The
// original top is unioned back in, so where it already sits within that band it is kept as-is.
// Never claims a bottom surface.
const double top_expansion = layerm->region().config().top_surface_expansion.value;
if (top_expansion > 0. && ! top.empty()) {
const double d = scale_(top_expansion);
const auto jt = Clipper2Lib::JoinType::Miter;
const ExPolygons T = union_ex(to_expolygons(top));
const int wall_loops = layerm->region().config().wall_loops.value;
// ORCA: Grow the top surfaces by top_surface_expansion, so the top solid infill also covers the
// material left by features rising from the middle of a top surface (filling the holes and
// joining the tops, so the features rest on solid infill). Each connected island is grown and
// clipped separately: growing one island's top across a gap into another - which may have no top
// surface at all, leaving a partially filled layer - is never allowed. The original top is
// unioned back in and bottom surfaces are never claimed, so this can only add area.
const PrintRegionConfig &region_config = layerm->region().config();
const double top_expansion = region_config.top_surface_expansion.value;
// Nothing to expand without a top fill: a 0% top surface density leaves the top layer with
// walls only, and zero top shell layers retypes it as internal in prepare_fill_surfaces().
if (top_expansion > 0. && region_config.top_shell_layers.value > 0 &&
region_config.top_surface_density.value > 0. && ! top.empty()) {
const double d = scale_(top_expansion);
const ExPolygons T = union_ex(to_expolygons(top));
// Walls are laid out on spacing, not width; and only_one_wall_top leaves a single wall over
// a top surface, which is exactly the situation handled here.
const int wall_loops = region_config.only_one_wall_top.value ? std::min(region_config.wall_loops.value, 1)
: region_config.wall_loops.value;
const double wall_band = wall_loops <= 0 ? 0. :
double(layerm->flow(frExternalPerimeter).scaled_width()) +
double(layerm->flow(frPerimeter).scaled_width()) * double(wall_loops - 1);
const double margin = scale_(layerm->region().config().top_surface_expansion_margin.value);
double(layerm->flow(frPerimeter).scaled_spacing()) * double(wall_loops - 1);
const double margin = scale_(region_config.top_surface_expansion_margin.value);
// minimum real top to act on: ignore anything thinner than ~2 top-infill lines
const float min_top = float(layerm->flow(frTopSolidInfill).scaled_width());
const auto direction = layerm->region().config().top_surface_expansion_direction.value;
const auto direction = region_config.top_surface_expansion_direction.value;
ExPolygons grown;
for (const ExPolygon &island : union_ex(layerm_slices_surfaces)) {
// The top infill only exists inside the perimeters, so seed and measure from the infill
// region (the island minus the wall band), not the raw slice. A section whose only
// exposed top lies in the wall band - i.e. a layer where the top is just the walls
// themselves - has no infill here and is skipped, instead of being flooded inward by
// the expansion. Thin slivers inside the infill region are dropped by the opening too.
// region (the island minus the wall band), not the raw slice: a section whose exposed top
// is just the walls themselves is then skipped instead of being flooded inward. Clip the
// layer's tops to the island first, to keep the boolean ops proportional to the island.
const ExPolygons infill_region = wall_band > 0. ? offset_ex(island, -float(wall_band)) : ExPolygons{ island };
const ExPolygons island_top = intersection_ex(T, infill_region);
const ExPolygons island_top = intersection_ex(
ClipperUtils::clip_clipper_polygons_with_subject_bbox(T, get_extents(island).inflated(SCALED_EPSILON)),
infill_region);
if (opening_ex(island_top, min_top).empty())
continue; // no real top infill in this section - never expand into it
// grow by d, then keep only the part allowed by the configured direction: inward fills
// the holes/gaps left by features (clip the growth back to the top's own filled outline,
// which leaves the outer edge fixed), outward grows the outer edge toward the walls (drop
// the growth that fell into the original holes), and inward+outward keeps both.
ExPolygons expanded = offset_ex_2(island_top, d, jt);
// Grow, then keep only what the configured direction allows, using the top's own filled
// outline (same outer edge, holes closed) to tell the two apart.
ExPolygons expanded = offset_ex_2(island_top, d, Clipper2Lib::JoinType::Miter);
if (direction != TopSurfaceExpansionDirection::InwardAndOutward) {
ExPolygons outline; // the top with its holes filled (same outer edge)
ExPolygons outline;
outline.reserve(island_top.size());
for (const ExPolygon &ex : island_top)
outline.emplace_back(ex.contour);
+25 -6
View File
@@ -10,6 +10,7 @@
#include "KDTreeIndirect.hpp"
#include "MutablePriorityQueue.hpp"
#include "Print.hpp"
#include "GCode/OrderingStrategies.hpp"
#include <cmath>
#include <cassert>
@@ -1103,7 +1104,7 @@ std::vector<size_t> chain_expolygons(const ExPolygons &input_exploy) {
return chain_points(points);
}
std::vector<size_t> chain_points(const Points &points, Point *start_near)
std::vector<size_t> chain_points(const Points &points, const Point *start_near)
{
auto segment_end_point = [&points](size_t idx, bool /* first_point */) -> const Point& { return points[idx]; };
std::vector<std::pair<size_t, bool>> ordered = chain_segments_greedy<Point, decltype(segment_end_point)>(segment_end_point, points.size(), start_near);
@@ -1111,9 +1112,26 @@ std::vector<size_t> chain_points(const Points &points, Point *start_near)
out.reserve(ordered.size());
for (auto &segment_and_reversal : ordered)
out.emplace_back(segment_and_reversal.first);
return out;
}
std::vector<size_t> chain_points_with_postprocessing(const Points &points, const Point *start_near)
{
std::vector<size_t> path = chain_points(points, start_near);
// Alternate 2-opt and crossing removal until convergence.
// 2-opt can create new crossings, and crossing removal can create new
// opportunities for 2-opt improvement. Break early if neither improves.
for (int iter = 0; iter < 3; ++iter) {
bool improved = tsp_2opt_improve(path, points);
improved |= tsp_remove_crossings(path, points);
if (!improved) break;
}
if (start_near == nullptr)
tsp_rotate_minimize_closing(path, points);
return path;
}
#ifndef NDEBUG
// #define DEBUG_SVG_OUTPUT
#endif /* NDEBUG */
@@ -2025,12 +2043,13 @@ std::vector<const PrintInstance*> chain_print_object_instances(const std::vector
instances.emplace_back(i, j);
}
}
auto segment_end_point = [&object_reference_points](size_t idx, bool /* first_point */) -> const Point& { return object_reference_points[idx]; };
std::vector<std::pair<size_t, bool>> ordered = chain_segments_greedy<Point, decltype(segment_end_point)>(segment_end_point, instances.size(), start_near);
// Order objects using nearest neighbor + post-processing (crossing removal + 2-opt).
std::vector<size_t> path = chain_points_with_postprocessing(object_reference_points, start_near);
std::vector<const PrintInstance*> out;
out.reserve(instances.size());
for (auto& segment_and_reversal : ordered) {
const std::pair<size_t, size_t>& inst = instances[segment_and_reversal.first];
out.reserve(path.size());
for (size_t idx : path) {
const std::pair<size_t, size_t>& inst = instances[idx];
out.emplace_back(&print_objects[inst.first]->instances()[inst.second]);
}
return out;
+3 -1
View File
@@ -15,7 +15,9 @@ namespace Slic3r {
using PolyNodes = std::vector<PolyNode*, PointsAllocator<PolyNode*>>;
}
std::vector<size_t> chain_points(const Points &points, Point *start_near = nullptr);
std::vector<size_t> chain_points(const Points &points, const Point *start_near = nullptr);
// Variant with post-processing (crossing removal + 2-opt) for object ordering.
std::vector<size_t> chain_points_with_postprocessing(const Points &points, const Point *start_near = nullptr);
std::vector<size_t> chain_expolygons(const ExPolygons &input_exploy);
std::vector<std::pair<size_t, bool>> chain_extrusion_entities(std::vector<ExtrusionEntity*> &entities, const Point *start_near = nullptr);
+55 -27
View File
@@ -65,6 +65,15 @@ std::pair<SupportGeneratorLayersPtr, SupportGeneratorLayersPtr> generate_interfa
const bool smooth_supports = support_params.support_style != smsGrid;
SupportGeneratorLayersPtr &interface_layers = base_and_interface_layers.first;
SupportGeneratorLayersPtr &base_interface_layers = base_and_interface_layers.second;
// The user-facing interface layer counts include the contact layer. Internally,
// contact layers are generated separately, so only the remaining layers are
// projected into intermediate interface/base-interface layers here.
const size_t num_top_interface_layers = support_params.has_top_contacts ? support_params.num_top_interface_layers - 1 : 0;
const size_t num_bottom_interface_layers = support_params.has_bottom_contacts ? support_params.num_bottom_interface_layers - 1 : 0;
const size_t num_top_base_interface_layers = std::min(support_params.num_top_base_interface_layers, num_top_interface_layers);
const size_t num_bottom_base_interface_layers = std::min(support_params.num_bottom_base_interface_layers, num_bottom_interface_layers);
const size_t num_top_interface_layers_only = num_top_interface_layers - num_top_base_interface_layers;
const size_t num_bottom_interface_layers_only = num_bottom_interface_layers - num_bottom_base_interface_layers;
interface_layers.assign(intermediate_layers.size(), nullptr);
if (support_params.has_base_interfaces())
@@ -124,6 +133,8 @@ std::pair<SupportGeneratorLayersPtr, SupportGeneratorLayersPtr> generate_interfa
};
tbb::parallel_for(tbb::blocked_range<int>(0, int(intermediate_layers.size())),
[&bottom_contacts, &top_contacts, &top_interface_layers, &top_base_interface_layers, &intermediate_layers, &insert_layer, &support_params,
num_top_interface_layers, num_bottom_interface_layers, num_top_base_interface_layers, num_bottom_base_interface_layers,
num_top_interface_layers_only, num_bottom_interface_layers_only,
snug_supports, &interface_layers, &base_interface_layers](const tbb::blocked_range<int>& range) {
// Gather the top / bottom contact layers intersecting with num_interface_layers resp. num_interface_layers_only intermediate layers above / below
// this intermediate layer.
@@ -142,16 +153,16 @@ std::pair<SupportGeneratorLayersPtr, SupportGeneratorLayersPtr> generate_interfa
Polygons polygons_top_contact_projected_base;
Polygons polygons_bottom_contact_projected_interface;
Polygons polygons_bottom_contact_projected_base;
if (support_params.num_top_interface_layers > 0) {
if (num_top_interface_layers > 0) {
// Top Z coordinate of a slab, over which we are collecting the top / bottom contact surfaces
coordf_t top_z = intermediate_layers[std::min(num_intermediate - 1, idx_intermediate_layer + int(support_params.num_top_interface_layers) - 1)]->print_z;
coordf_t top_inteface_z = std::numeric_limits<coordf_t>::max();
if (support_params.num_top_base_interface_layers > 0)
coordf_t top_z = intermediate_layers[std::min(num_intermediate - 1, idx_intermediate_layer + int(num_top_interface_layers) - 1)]->print_z;
coordf_t top_interface_z = std::numeric_limits<coordf_t>::max();
if (num_top_base_interface_layers > 0)
// Some top base interface layers will be generated.
top_inteface_z = support_params.num_top_interface_layers_only() == 0 ?
top_interface_z = num_top_interface_layers_only == 0 ?
// Only base interface layers to generate.
- std::numeric_limits<coordf_t>::max() :
intermediate_layers[std::min(num_intermediate - 1, idx_intermediate_layer + int(support_params.num_top_interface_layers_only()) - 1)]->print_z;
intermediate_layers[std::min(num_intermediate - 1, idx_intermediate_layer + int(num_top_interface_layers_only) - 1)]->print_z;
// Move idx_top_contact_first up until above the current print_z.
idx_top_contact_first = idx_higher_or_equal(top_contacts, idx_top_contact_first, [&intermediate_layer](const SupportGeneratorLayer *layer){ return layer->print_z >= intermediate_layer.print_z; }); // - EPSILON
// Collect the top contact areas above this intermediate layer, below top_z.
@@ -160,22 +171,22 @@ std::pair<SupportGeneratorLayersPtr, SupportGeneratorLayersPtr> generate_interfa
//FIXME maybe this adds one interface layer in excess?
if (top_contact_layer.bottom_z - EPSILON > top_z)
break;
polygons_append(top_contact_layer.bottom_z - EPSILON > top_inteface_z ? polygons_top_contact_projected_base : polygons_top_contact_projected_interface,
polygons_append(top_contact_layer.bottom_z - EPSILON > top_interface_z ? polygons_top_contact_projected_base : polygons_top_contact_projected_interface,
// For snug supports, project the overhang polygons covering the whole overhang, so that they will merge without a gap with support polygons of the other layers.
// For grid supports, merging of support regions will be performed by the projection into grid.
snug_supports ? *top_contact_layer.overhang_polygons : top_contact_layer.polygons);
}
}
if (support_params.num_bottom_interface_layers > 0) {
if (num_bottom_interface_layers > 0) {
// Bottom Z coordinate of a slab, over which we are collecting the top / bottom contact surfaces
coordf_t bottom_z = intermediate_layers[std::max(0, idx_intermediate_layer - int(support_params.num_bottom_interface_layers) + 1)]->bottom_z;
coordf_t bottom_z = intermediate_layers[std::max(0, idx_intermediate_layer - int(num_bottom_interface_layers) + 1)]->bottom_z;
coordf_t bottom_interface_z = - std::numeric_limits<coordf_t>::max();
if (support_params.num_bottom_base_interface_layers > 0)
if (num_bottom_base_interface_layers > 0)
// Some bottom base interface layers will be generated.
bottom_interface_z = support_params.num_bottom_interface_layers_only() == 0 ?
bottom_interface_z = num_bottom_interface_layers_only == 0 ?
// Only base interface layers to generate.
std::numeric_limits<coordf_t>::max() :
intermediate_layers[std::max(0, idx_intermediate_layer - int(support_params.num_bottom_interface_layers_only()))]->bottom_z;
intermediate_layers[std::max(0, idx_intermediate_layer - int(num_bottom_interface_layers_only))]->bottom_z;
// Move idx_bottom_contact_first up until touching bottom_z.
idx_bottom_contact_first = idx_higher_or_equal(bottom_contacts, idx_bottom_contact_first, [bottom_z](const SupportGeneratorLayer *layer){ return layer->print_z >= bottom_z - EPSILON; });
// Collect the top contact areas above this intermediate layer, below top_z.
@@ -1563,13 +1574,17 @@ void generate_support_toolpaths(
// Pointer to the 1st layer interface filler.
auto filler_first_layer = filler_first_layer_ptr ? filler_first_layer_ptr.get() : filler_interface.get();
// Filler for the 1st layer interface, if different from filler_interface.
auto filler_raft_contact_ptr = std::unique_ptr<Fill>(range.begin() == n_raft_layers && config.support_interface_top_layers.value == 0 ?
const bool top_interfaces_enabled = support_params.num_top_interface_layers > 0;
const bool bottom_interfaces_enabled = support_params.num_bottom_interface_layers > 0;
const coordf_t base_interface_density = top_interfaces_enabled || !bottom_interfaces_enabled ?
support_params.top_interface_density : support_params.bottom_interface_density;
auto filler_raft_contact_ptr = std::unique_ptr<Fill>(range.begin() == n_raft_layers && !top_interfaces_enabled ?
Fill::new_from_type(support_params.raft_interface_fill_pattern) : nullptr);
// Pointer to the 1st layer interface filler.
auto filler_raft_contact = filler_raft_contact_ptr ? filler_raft_contact_ptr.get() : filler_interface.get();
// Filler for the base interface (to be used for soluble interface / non soluble base, to produce non soluble interface layer below soluble interface layer).
auto filler_base_interface = std::unique_ptr<Fill>(base_interface_layers.empty() ? nullptr :
Fill::new_from_type(support_params.top_interface_density > 0.95 || support_params.with_sheath ? ipRectilinear : ipSupportBase));
Fill::new_from_type(base_interface_density > 0.95 || support_params.with_sheath ? ipRectilinear : ipSupportBase));
auto filler_support = std::unique_ptr<Fill>(Fill::new_from_type(support_params.base_fill_pattern));
filler_interface->set_bounding_box(bbox_object);
if (filler_first_layer_ptr)
@@ -1583,10 +1598,7 @@ void generate_support_toolpaths(
{
SupportLayer &support_layer = *support_layers[support_layer_id];
LayerCache &layer_cache = layer_caches[support_layer_id];
const float support_interface_angle = (config.support_interface_pattern == smipRectilinearInterlaced) ?
support_params.raft_interface_angle(support_layer.interface_id()) :
((support_params.support_style == smsGrid || config.support_interface_pattern == smipRectilinear) ?
support_params.interface_angle : support_params.raft_interface_angle(support_layer.interface_id()));
const float support_interface_angle = support_params.support_interface_angle(support_layer.interface_id());
// Find polygons with the same print_z.
SupportGeneratorLayerExtruded &bottom_contact_layer = layer_cache.bottom_contact_layer;
@@ -1619,7 +1631,9 @@ void generate_support_toolpaths(
bool raft_layer = slicing_params.interface_raft_layers && top_contact_layer.layer && is_approx(top_contact_layer.layer->print_z, slicing_params.raft_contact_top_z);
// ORCA: Organic tree uses projected contacts to build the interface stack; avoid extra bottom-contact extrusion.
const bool organic_tree = support_params.support_style == SupportMaterialStyle::smsTreeOrganic;
if (config.support_interface_top_layers == 0) {
const bool top_interfaces = support_params.num_top_interface_layers > 0;
const bool bottom_interfaces = support_params.num_bottom_interface_layers > 0;
if (!top_interfaces) {
// If no top interface layers were requested, we treat the contact layer exactly as a generic base layer.
// Don't merge the raft contact layer though.
if (support_params.can_merge_support_regions && ! raft_layer) {
@@ -1642,15 +1656,29 @@ void generate_support_toolpaths(
if (top_contact_layer.could_merge(interface_layer) && ! raft_layer)
top_contact_layer.merge(std::move(interface_layer));
}
if ((config.support_interface_top_layers == 0 || config.support_interface_bottom_layers == 0) && support_params.can_merge_support_regions) {
if (!bottom_interfaces && support_params.can_merge_support_regions) {
if (base_layer.could_merge(bottom_contact_layer))
base_layer.merge(std::move(bottom_contact_layer));
else if (base_layer.empty() && ! bottom_contact_layer.empty() && ! bottom_contact_layer.layer->bridging)
base_layer = std::move(bottom_contact_layer);
} else if (bottom_contact_layer.could_merge(top_contact_layer) && ! raft_layer) {
top_contact_layer.merge(std::move(bottom_contact_layer));
if (top_interfaces && bottom_interfaces) {
top_contact_layer.merge(std::move(bottom_contact_layer));
} else if (bottom_interfaces) {
top_contact_layer.set_polygons_to_extrude(
diff(top_contact_layer.polygons_to_extrude(), bottom_contact_layer.polygons_to_extrude()));
} else {
bottom_contact_layer.set_polygons_to_extrude(
diff(bottom_contact_layer.polygons_to_extrude(), top_contact_layer.polygons_to_extrude()));
}
} else if (bottom_contact_layer.could_merge(interface_layer) && ! organic_tree) {
bottom_contact_layer.merge(std::move(interface_layer));
const bool interface_layer_is_bottom = interface_layer.layer->layer_type == SupporLayerType::BottomInterface;
if (bottom_interfaces && interface_layer_is_bottom) {
bottom_contact_layer.merge(std::move(interface_layer));
} else {
bottom_contact_layer.set_polygons_to_extrude(
diff(bottom_contact_layer.polygons_to_extrude(), interface_layer.polygons_to_extrude()));
}
}
// Orca: For organic trees the support-material regions are generated from
@@ -1730,12 +1758,12 @@ void generate_support_toolpaths(
interface_as_base ? ExtrusionRole::erSupportMaterial : ExtrusionRole::erSupportMaterialInterface, interface_flow);
}
};
const bool top_interfaces = support_params.num_top_interface_layers > 0;
const bool bottom_interfaces = top_interfaces && support_params.num_bottom_interface_layers > 0;
extrude_interface(top_contact_layer, raft_layer ? InterfaceLayerType::RaftContact : top_interfaces ? InterfaceLayerType::TopContact : InterfaceLayerType::InterfaceAsBase);
if (!organic_tree)
extrude_interface(bottom_contact_layer, bottom_interfaces ? InterfaceLayerType::BottomContact : InterfaceLayerType::InterfaceAsBase);
extrude_interface(interface_layer, top_interfaces ? InterfaceLayerType::Interface : InterfaceLayerType::InterfaceAsBase);
const bool interface_layer_enabled = !interface_layer.empty() &&
(interface_layer.layer->layer_type == SupporLayerType::BottomInterface ? bottom_interfaces : top_interfaces);
extrude_interface(interface_layer, interface_layer_enabled ? InterfaceLayerType::Interface : InterfaceLayerType::InterfaceAsBase);
// Base interface layers under soluble interfaces
if ( ! base_interface_layer.empty() && ! base_interface_layer.polygons_to_extrude().empty()) {
Fill *filler = filler_base_interface.get();
@@ -1745,7 +1773,7 @@ void generate_support_toolpaths(
Flow interface_flow = support_params.support_material_flow.with_height(float(base_interface_layer.layer->height));
filler->angle = support_interface_angle;
filler->spacing = support_params.support_material_interface_flow.spacing();
filler->link_max_length = coord_t(scale_(filler->spacing * link_max_length_factor / support_params.top_interface_density));
filler->link_max_length = coord_t(scale_(filler->spacing * link_max_length_factor / base_interface_density));
fill_expolygons_generate_paths(
// Destination
base_interface_layer.extrusions,
@@ -1753,7 +1781,7 @@ void generate_support_toolpaths(
// Regions to fill
union_safety_offset_ex(base_interface_layer.polygons_to_extrude()),
// Filler and its parameters
filler, float(support_params.top_interface_density),
filler, float(base_interface_density),
// Extrusion parameters
ExtrusionRole::erSupportMaterial, interface_flow);
}
+45 -15
View File
@@ -34,7 +34,7 @@ struct SupportParameters {
{
this->num_top_interface_layers = std::max(0, object_config.support_interface_top_layers.value);
this->num_bottom_interface_layers = number_of_support_interface_bottom_layers(object_config);
this->num_bottom_interface_layers = std::max(0, number_of_support_interface_bottom_layers(object_config));
this->has_top_contacts = num_top_interface_layers > 0;
this->has_bottom_contacts = num_bottom_interface_layers > 0;
// BBS: if support interface and support base do not use the same filament, add a base layer to improve their adhesion
@@ -46,15 +46,15 @@ struct SupportParameters {
if (non_soluble_base_top) { // ORCA: Try to support soluble dense interfaces with non-soluble dense interfaces.
this->num_top_base_interface_layers = size_t(std::min(int(num_top_interface_layers) / 2, 2));
} else {
this->num_top_base_interface_layers =
(different_support_interface_filament && this->zero_gap_interface_top) ? 1 : 0;
// Keep at least one configured layer on the interface filament.
this->num_top_base_interface_layers = different_support_interface_filament && num_top_interface_layers > 1 ? 1 : 0;
}
if (non_soluble_base_bottom) { // ORCA: Try to support soluble dense interfaces with non-soluble dense interfaces.
this->num_bottom_base_interface_layers = size_t(std::min(int(num_bottom_interface_layers) / 2, 2));
} else {
this->num_bottom_base_interface_layers =
(different_support_interface_filament && this->zero_gap_interface_bottom) ? 1 : 0;
// Keep at least one configured layer on the interface filament.
this->num_bottom_base_interface_layers = different_support_interface_filament && num_bottom_interface_layers > 1 ? 1 : 0;
}
}
this->first_layer_flow = Slic3r::support_material_1st_layer_flow(&object, float(slicing_params.first_print_layer_height));
@@ -74,7 +74,7 @@ struct SupportParameters {
for (auto layer : object.layers())
this->support_layer_height_min = std::min(this->support_layer_height_min, std::max(0.01, layer->height));
if (object_config.support_interface_top_layers.value == 0) {
if (this->num_top_interface_layers == 0 && this->num_bottom_interface_layers == 0) {
// No interface layers allowed, print everything with the base support pattern.
this->support_material_interface_flow = this->support_material_flow;
}
@@ -120,8 +120,8 @@ struct SupportParameters {
this->raft_interface_density = std::min(1., this->raft_interface_flow.spacing() / raft_interface_spacing);
this->support_spacing = object_config.support_base_pattern_spacing.value + this->support_material_flow.spacing();
this->support_density = std::min(1., this->support_material_flow.spacing() / this->support_spacing);
if (object_config.support_interface_top_layers.value == 0) {
// No interface layers allowed, print everything with the base support pattern.
if (this->num_top_interface_layers == 0) {
// No top interface layers allowed; keep unused top interface parameters aligned with base support.
this->top_interface_spacing = this->support_spacing;
this->top_interface_density = this->support_density;
}
@@ -133,16 +133,20 @@ struct SupportParameters {
this->support_density > 0.95 || this->with_sheath ? ipRectilinear : ipSupportBase;
this->interface_fill_pattern = (this->top_interface_density > 0.95 ? ipRectilinear : ipSupportBase);
this->raft_interface_fill_pattern = this->raft_interface_density > 0.95 ? ipRectilinear : ipSupportBase;
const coordf_t contact_interface_density = this->num_top_interface_layers > 0 ?
this->top_interface_density : this->bottom_interface_density;
const bool zero_gap_contact_interface = this->num_top_interface_layers > 0 ?
this->zero_gap_interface_top : this->zero_gap_interface_bottom;
if (object_config.support_interface_pattern == smipGrid)
this->contact_fill_pattern = ipGrid;
else if (object_config.support_interface_pattern == smipRectilinearInterlaced)
this->contact_fill_pattern = ipRectilinear;
else
this->contact_fill_pattern =
(object_config.support_interface_pattern == smipAuto && this->zero_gap_interface_top) ||
(object_config.support_interface_pattern == smipAuto && zero_gap_contact_interface) ||
object_config.support_interface_pattern == smipConcentric ?
ipConcentric :
(this->top_interface_density > 0.95 ? ipRectilinear : ipSupportBase);
(contact_interface_density > 0.95 ? ipRectilinear : ipSupportBase);
this->raft_angle_1st_layer = 0.f;
this->raft_angle_base = 0.f;
@@ -188,6 +192,7 @@ struct SupportParameters {
std::numeric_limits<double>::max();
support_style = object_config.support_style;
support_interface_pattern = object_config.support_interface_pattern;
if (support_style != smsDefault) {
if ((support_style == smsSnug || support_style == smsGrid) && is_tree(object_config.support_type)) support_style = smsDefault;
if ((support_style == smsTreeSlim || support_style == smsTreeStrong || support_style == smsTreeHybrid || support_style == smsTreeOrganic) &&
@@ -211,9 +216,9 @@ struct SupportParameters {
bool has_top_contacts;
// Is there at least a bottom contact layer extruded below support base?
bool has_bottom_contacts;
// Number of top interface layers without counting the contact layer.
// User-configured number of top interface layers, including the contact layer.
size_t num_top_interface_layers;
// Number of bottom interface layers without counting the contact layer.
// User-configured number of bottom interface layers, including the contact layer.
size_t num_bottom_interface_layers;
// Number of top base interface layers.
size_t num_top_base_interface_layers;
@@ -235,7 +240,7 @@ struct SupportParameters {
Flow support_material_interface_flow;
// Flow at the bottom interfaces and contacts.
Flow support_material_bottom_interface_flow;
// Flow at raft inteface & contact layers.
// Flow at raft interface & contact layers.
Flow raft_interface_flow;
coordf_t support_extrusion_width;
// Is merging of regions allowed? Could the interface & base support regions be printed with the same extruder?
@@ -262,6 +267,7 @@ struct SupportParameters {
// Density of the base support layers.
coordf_t support_density;
SupportMaterialStyle support_style = smsDefault;
SupportMaterialInterfacePattern support_interface_pattern = smipAuto;
// Pattern of the sparse infill including sparse raft layers.
InfillPattern base_fill_pattern;
@@ -280,9 +286,33 @@ struct SupportParameters {
float raft_angle_base;
float raft_angle_interface;
// Produce a raft interface angle for a given SupportLayer::interface_id()
// Produce a +/-45deg alternating raft interface angle for a given SupportLayer::interface_id().
float raft_interface_angle(size_t interface_id) const
{ return this->raft_angle_interface + ((interface_id & 1) ? float(- M_PI / 4.) : float(+ M_PI / 4.)); }
{ return this->raft_angle_interface + ((interface_id & 1) ? float(- M_PI_4) : float(+ M_PI_4)); }
// Produce support interface angle for a given SupportLayer::interface_id().
// Angle will be shifted/rotated based on interface pattern.
float support_interface_angle(size_t interface_id) const
{
float angle;
switch (this->support_interface_pattern) {
case SupportMaterialInterfacePattern::smipRectilinear:
angle = support_style == SupportMaterialStyle::smsSnug ? this->interface_angle - float(M_PI_4) : this->interface_angle;
break;
case SupportMaterialInterfacePattern::smipRectilinearInterlaced:
angle = this->interface_angle + ((interface_id & 1) ? float(M_PI_4) : float(-M_PI_4));
break;
case SupportMaterialInterfacePattern::smipGrid:
angle = this->base_angle;
break;
default:
angle = this->interface_angle;
break;
}
return angle;
}
bool independent_layer_height = false;
const double thresh_big_overhang = Slic3r::sqr(scale_(10));
+1 -1
View File
@@ -469,7 +469,7 @@ void TreeModelVolumes::calculateCollision(const coord_t radius, const LayerIndex
});
// 2) Sum over top / bottom ranges.
const bool processing_last_mesh = outline_idx == layer_outline_indices.size();
const bool processing_last_mesh = outline_idx == layer_outline_indices.back();
tbb::parallel_for(tbb::blocked_range<LayerIndex>(data.begin(), data.end()),
[&collision_areas_offsetted, &outlines, &machine_border = m_machine_border, &anti_overhang = m_anti_overhang, radius,
xy_distance, z_distance_bottom_layers, z_distance_top_layers, min_resolution = m_min_resolution, &data, processing_last_mesh, &throw_on_cancel]
+40 -54
View File
@@ -1511,7 +1511,9 @@ void TreeSupport::generate_toolpaths()
// ORCA: reset interface Fill state per area group to keep angles deterministic.
filler_interface->fixed_angle = false;
filler_interface->layer_id = size_t(-1);
filler_interface->angle = base_support_angle + M_PI_2; // default interface angle is perpendicular to support angle
filler_Roof1stLayer->fixed_angle = false;
filler_Roof1stLayer->layer_id = size_t(-1);
filler_interface->angle = m_support_params.support_interface_angle(area_group.interface_id);
if (area_group.type != SupportLayer::BaseType) {
// interface
if (layer_id == 0) {
@@ -1537,8 +1539,10 @@ void TreeSupport::generate_toolpaths()
fill_params.density = interface_density;
// Note: spacing means the separation between two lines as if they are tightly extruded
filler_Roof1stLayer->spacing = interface_flow.spacing();
filler_Roof1stLayer->angle = base_support_angle;
filler_Roof1stLayer->angle = m_support_params.support_interface_angle(area_group.interface_id);
fill_params.dont_sort = true;
filler_Roof1stLayer->fixed_angle = (m_object_config->support_interface_pattern == smipRectilinearInterlaced ||
m_object_config->support_interface_pattern == smipRectilinear);
Flow interface_base_flow = interface_as_base ? support_flow : interface_flow;
ExtrusionRole interface_role = interface_as_base ? erSupportMaterial : erSupportMaterialInterface;
// generate a perimeter first to support interface better
@@ -1556,18 +1560,11 @@ void TreeSupport::generate_toolpaths()
fill_params.density = bottom_interface_density;
filler_interface->spacing = interface_flow.spacing();
if (m_object_config->support_interface_pattern == smipGrid) {
filler_interface->angle = base_support_angle;
fill_params.dont_sort = true;
}
if (m_object_config->support_interface_pattern == smipRectilinearInterlaced) {
// ORCA: explicit 0/90 alternation for rectilinear interlaced interfaces.
filler_interface->fixed_angle = true;
filler_interface->angle = base_support_angle + ((area_group.interface_id & 1) * M_PI_2);
fill_params.dont_sort = true;
}
fill_params.dont_sort = (m_object_config->support_interface_pattern == smipGrid ||
m_object_config->support_interface_pattern == smipRectilinearInterlaced);
filler_interface->fixed_angle = (m_object_config->support_interface_pattern == smipRectilinearInterlaced ||
m_object_config->support_interface_pattern == smipRectilinear);
Flow interface_base_flow = interface_as_base ? support_flow : interface_flow;
ExtrusionRole interface_role = interface_as_base ? erSupportMaterial : erSupportMaterialInterface;
@@ -1579,17 +1576,11 @@ void TreeSupport::generate_toolpaths()
fill_params.density = interface_density;
filler_interface->spacing = interface_flow.spacing();
if (m_object_config->support_interface_pattern == smipGrid) {
filler_interface->angle = base_support_angle;
fill_params.dont_sort = true;
}
fill_params.dont_sort = (m_object_config->support_interface_pattern == smipGrid ||
m_object_config->support_interface_pattern == smipRectilinearInterlaced);
if (m_object_config->support_interface_pattern == smipRectilinearInterlaced) {
// ORCA: explicit 0/90 alternation for rectilinear interlaced interfaces.
filler_interface->fixed_angle = true;
filler_interface->angle = base_support_angle + ((area_group.interface_id & 1) * M_PI_2);
fill_params.dont_sort = true;
}
filler_interface->fixed_angle = (m_object_config->support_interface_pattern == smipRectilinearInterlaced ||
m_object_config->support_interface_pattern == smipRectilinear);
Flow interface_base_flow = interface_as_base ? support_flow : interface_flow;
ExtrusionRole interface_role = interface_as_base ? erSupportMaterial : erSupportMaterialInterface;
@@ -2014,6 +2005,9 @@ void TreeSupport::draw_circles()
// generate areas
const coordf_t layer_height = config.layer_height.value;
const size_t top_interface_layers = m_support_params.num_top_interface_layers;
const int top_base_interface_layers = std::min<int>(
int(m_support_params.num_top_base_interface_layers),
top_interface_layers > 0 ? int(top_interface_layers) - 1 : 0);
const size_t bottom_interface_layers = number_of_support_interface_bottom_layers(config);
const double nozzle_diameter = m_object->print()->config().nozzle_diameter.get_at(0);
const coordf_t line_width = config.get_abs_value("support_line_width", nozzle_diameter);
@@ -2054,12 +2048,14 @@ void TreeSupport::draw_circles()
ExPolygons& base_areas = ts_layer->base_areas;
ExPolygons& roof_areas = ts_layer->roof_areas;
ExPolygons roof_base_areas;
ExPolygons& roof_1st_layer = ts_layer->roof_1st_layer;
ExPolygons& floor_areas = ts_layer->floor_areas;
ExPolygons& roof_gap_areas = ts_layer->roof_gap_areas;
coordf_t max_layers_above_base = 0;
coordf_t max_layers_above_roof = 0;
coordf_t max_layers_above_roof1 = 0;
size_t first_base_roof_area = 0;
bool floor_interface_as_base = false;
bool has_circle_node = false;
bool need_extra_wall = false;
@@ -2094,8 +2090,6 @@ void TreeSupport::draw_circles()
break;
const SupportNode& node = *p_node;
// ORCA: Cap top interface height in mm based on per-node support layer height.
const coordf_t top_interface_height = coordf_t(top_interface_layers) * node.height;
ExPolygons area;
// Generate directly from overhang polygon if one of the following is true:
// 1) node is a normal part of hybrid support
@@ -2159,18 +2153,16 @@ void TreeSupport::draw_circles()
if (obj_layer_nr>0 && node.distance_to_top < 0)
append(roof_gap_areas, area);
// ORCA: Roof1stLayer must also fit inside the mm cap.
else if (obj_layer_nr > 0 && node.support_roof_layers_below == 1 &&
(node.dist_mm_to_top - this->top_z_distance) < top_interface_height + EPSILON && node.is_sharp_tail==false)
node.is_sharp_tail == false)
{
append(roof_1st_layer, area);
max_layers_above_roof1 = std::max(max_layers_above_roof1, node.dist_mm_to_top);
}
// ORCA: Roof layers must also fit inside the mm cap.
else if (obj_layer_nr > 0 && node.support_roof_layers_below > 1 &&
(node.dist_mm_to_top - this->top_z_distance) < top_interface_height + EPSILON && node.is_sharp_tail == false)
node.is_sharp_tail == false)
{
append(roof_areas, area);
append(node.support_roof_layers_below <= top_base_interface_layers ? roof_base_areas : roof_areas, area);
max_layers_above_roof = std::max(max_layers_above_roof, node.dist_mm_to_top);
}
else
@@ -2184,9 +2176,17 @@ void TreeSupport::draw_circles()
//m_object->print()->set_status(65, (boost::format( _u8L("Support: generate polygons at layer %d")) % layer_nr).str());
// join roof segments
roof_areas = diff_clipped(offset2_ex(roof_areas, line_width_scaled, -line_width_scaled), get_collision(false));
roof_areas = diff_clipped(closing_ex(roof_areas, line_width_scaled), get_collision(false));
roof_areas = intersection_ex(roof_areas, m_machine_border);
roof_1st_layer = diff_clipped(offset2_ex(roof_1st_layer, line_width_scaled, -line_width_scaled), get_collision(false));
roof_base_areas = diff_clipped(closing_ex(roof_base_areas, line_width_scaled), get_collision(false));
roof_base_areas = intersection_ex(roof_base_areas, m_machine_border);
if (!roof_base_areas.empty() && !roof_areas.empty())
roof_base_areas = diff_ex(roof_base_areas,
ClipperUtils::clip_clipper_polygons_with_subject_bbox(roof_areas, get_extents(roof_base_areas)));
first_base_roof_area = roof_areas.size();
append(roof_areas, std::move(roof_base_areas));
roof_1st_layer = diff_clipped(closing_ex(roof_1st_layer, line_width_scaled), get_collision(false));
// roof_1st_layer and roof_areas may intersect, so need to subtract roof_areas from roof_1st_layer
roof_1st_layer = diff_ex(roof_1st_layer, ClipperUtils::clip_clipper_polygons_with_subject_bbox(roof_areas,get_extents(roof_1st_layer)));
@@ -2366,9 +2366,11 @@ void TreeSupport::draw_circles()
area_groups.back().need_infill = overlaps({ expoly }, area_poly);
area_groups.back().need_extra_wall = need_extra_wall && !area_groups.back().need_infill;
}
for (auto& expoly : ts_layer->roof_areas) {
for (size_t roof_idx = 0; roof_idx < ts_layer->roof_areas.size(); ++roof_idx) {
auto &expoly = ts_layer->roof_areas[roof_idx];
//if (area(expoly) < SQ(scale_(1))) continue;
area_groups.emplace_back(&expoly, SupportLayer::RoofType, max_layers_above_roof);
area_groups.back().interface_as_base = roof_idx >= first_base_roof_area;
}
for (auto &expoly : ts_layer->floor_areas) {
//if (area(expoly) < SQ(scale_(1))) continue;
@@ -2378,6 +2380,7 @@ void TreeSupport::draw_circles()
for (auto &expoly : ts_layer->roof_1st_layer) {
//if (area(expoly) < SQ(scale_(1))) continue;
area_groups.emplace_back(&expoly, SupportLayer::Roof1stLayer, max_layers_above_roof1);
area_groups.back().interface_as_base = top_base_interface_layers > 0;
}
for (auto &area_group : area_groups) {
@@ -2406,7 +2409,6 @@ void TreeSupport::draw_circles()
}
});
// ORCA: normalize interface_id sequencing to follow printed interface layers only.
const int top_base_layers = int(m_support_params.num_top_base_interface_layers);
const bool interlaced = m_object_config->support_interface_pattern == smipRectilinearInterlaced;
int roof_interface_id = 0;
int floor_interface_id = 0;
@@ -2425,7 +2427,6 @@ void TreeSupport::draw_circles()
if (area_group.type == SupportLayer::RoofType || area_group.type == SupportLayer::Roof1stLayer) {
if (interlaced)
area_group.interface_id = roof_interface_id;
area_group.interface_as_base = top_base_layers > 0 && roof_interface_id < top_base_layers;
has_roof_interface = true;
} else if (area_group.type == SupportLayer::FloorType) {
if (interlaced)
@@ -2897,7 +2898,7 @@ void TreeSupport::drop_nodes()
node_parent->merged_neighbours.push_front(node_parent == p_node ? neighbour : p_node);
const bool to_buildplate = !is_inside_ex(get_collision(0, obj_layer_nr_next), next_position);
SupportNode* next_node = m_ts_data->create_node(next_position, node_parent->distance_to_top + 1, obj_layer_nr_next,
node_parent->support_roof_layers_below - (node_parent->distance_to_top > 0 ? 1 : 0),
node_parent->support_roof_layers_below - (node_parent->distance_to_top >= 0 ? 1 : 0),
to_buildplate, node_parent, print_z_next, height_next);
get_max_move_dist(next_node);
m_ts_data->m_mutex.lock();
@@ -2949,7 +2950,7 @@ void TreeSupport::drop_nodes()
for(auto& overhang:overhangs_next) {
Point next_pt = overhang.contour.centroid();
SupportNode *next_node = m_ts_data->create_node(next_pt, p_node->distance_to_top + 1, obj_layer_nr_next,
p_node->support_roof_layers_below - (p_node->distance_to_top > 0 ? 1 : 0),
p_node->support_roof_layers_below - (p_node->distance_to_top >= 0 ? 1 : 0),
to_buildplate, p_node, print_z_next, height_next);
next_node->max_move_dist = 0;
next_node->overhang = std::move(overhang);
@@ -3096,7 +3097,7 @@ void TreeSupport::drop_nodes()
auto next_collision = get_collision(0, obj_layer_nr_next);
const bool to_buildplate = !is_inside_ex(m_ts_data->m_layer_outlines[obj_layer_nr_next], next_layer_vertex);
SupportNode * next_node = m_ts_data->create_node(next_layer_vertex, node.distance_to_top + 1, obj_layer_nr_next,
node.support_roof_layers_below - (node.distance_to_top > 0 ? 1 : 0),
node.support_roof_layers_below - (node.distance_to_top >= 0 ? 1 : 0),
to_buildplate, p_node, print_z_next, height_next);
// don't increase radius if next node will collide partially with the object (STUDIO-7883)
to_outside = projection_onto(next_collision, next_node->position);
@@ -3376,21 +3377,6 @@ std::vector<LayerHeightData> TreeSupport::plan_layer_heights()
}
}
// ORCA: Recompute support_roof_layers_below from remaining interface height (independent heights).
const int top_layers = m_object->config().support_interface_top_layers.value;
if (m_support_params.independent_layer_height && top_layers > 0) {
const coordf_t interface_height_mm = coordf_t(top_layers) * m_slicing_params.layer_height;
for (int layer_nr = 0; layer_nr < contact_nodes.size(); layer_nr++) {
if (contact_nodes[layer_nr].empty()) continue;
for (SupportNode *node : contact_nodes[layer_nr]) {
if (node->height <= EPSILON) continue;
const coordf_t remaining_mm = interface_height_mm - (node->dist_mm_to_top - this->top_z_distance);
const int layers_fit = remaining_mm < -EPSILON ? 0 : int(std::floor((remaining_mm + EPSILON) / node->height));
node->support_roof_layers_below = std::min(layers_fit, top_layers);
}
}
}
// log layer_heights
for (size_t i = 0; i < layer_heights.size(); i++) {
//if (layer_heights[i].height > EPSILON)
@@ -3498,7 +3484,7 @@ void TreeSupport::generate_contact_points()
if (force_add || !already_inserted.count(hash_pos)) {
already_inserted.emplace(hash_pos);
bool to_buildplate = true;
size_t roof_layers = add_interface ? (support_roof_layers > 0 ? support_roof_layers - 1 : 0) : 0; // subtract 1 because the contact node itself counts as one layer
size_t roof_layers = add_interface ? support_roof_layers : 0;
// add a new node as a virtual node which acts as the invisible gap between support and object
// distance_to_top=-1: it's virtual
// print_z=object_layer->bottom_z: it directly contacts the bottom
+1 -1
View File
@@ -706,7 +706,7 @@ static std::optional<std::pair<Point, size_t>> polyline_sample_next_point_at_dis
filler->spacing = flow.spacing();
filler->angle = roof ?
//fixme support_layer.interface_id() instead of layer_idx
(support_params.interface_angle + (layer_idx & 1) ? float(- M_PI / 4.) : float(+ M_PI / 4.)) :
(support_params.interface_angle + ((layer_idx & 1) ? float(- M_PI_4) : float(+ M_PI_4))) :
support_params.base_angle;
// ORCA: use top-specific interface density after separating top/bottom settings.
+2 -2
View File
@@ -62,7 +62,7 @@ struct TreeSupportMeshGroupSettings {
this->support_line_width = support_material_flow(&print_object, config.layer_height).scaled_width();
this->support_roof_line_width = support_material_interface_flow(&print_object, config.layer_height).scaled_width();
const int bottom_interface_layers = number_of_support_interface_bottom_layers(config);
this->support_bottom_enable = config.support_interface_top_layers.value > 0 && bottom_interface_layers > 0;
this->support_bottom_enable = bottom_interface_layers > 0;
this->support_bottom_height = this->support_bottom_enable ?
bottom_interface_layers * this->layer_height :
0;
@@ -705,7 +705,7 @@ public:
SupportGeneratorLayersPtr& top_contacts_mutable() { return this->top_contacts; }
public:
// Insert the contact layer and some of the inteface and base interface layers below.
// Insert the contact layer and some of the interface and base interface layers below.
void add_roofs(std::vector<Polygons> &&new_roofs, const size_t insert_layer_idx)
{
if (! new_roofs.empty()) {
+5
View File
@@ -51,4 +51,9 @@
// Enable extension of tool position imgui dialog to show actual speed profile
#define ENABLE_ACTUAL_SPEED_DEBUG 1
// Disable layout inspector for public release
#if BBL_RELEASE_TO_PUBLIC
#define WXINSPECTOR_DISABLE
#endif
#endif // _prusaslicer_technologies_h_
+12
View File
@@ -42,10 +42,22 @@ struct Calib_Params
std::string shaper_type;
std::vector<double> accelerations;
std::vector<double> speeds;
// Resolved layer height for the VFA tower (0 = auto: nozzle_diameter / 2). Each speed block is a
// fixed number of layers tall, so this also determines the physical block height / tower height.
double vfa_layer_height = 0.0;
// Scale the calibration model to the nozzle diameter and set the layer height accordingly (temp tower / VFA).
// When false the 0.4 mm / 0.2 mm reference model is printed as-is.
bool nozzle_based_resize = true;
CalibMode mode;
};
// Number of printed layers per speed block in the VFA tower. The base model has 5 mm blocks designed
// for a 0.2 mm layer height (0.4 mm nozzle), i.e. 25 layers per block.
static constexpr int vfa_layers_per_block = 25;
static constexpr double vfa_base_block_height = 5.0;
static constexpr double vfa_base_nozzle_diameter = 0.4;
enum FlowRatioCalibrationType {
COMPLETE_CALIBRATION = 0,
FINE_CALIBRATION,
+6 -3
View File
@@ -91,12 +91,15 @@ public:
//
void toggle_top_layer_only_view_range();
//
// Dim previous layers (ORCA, ported from preFlight)
// Whether the layers below the current top layer are rendered darkened while
// scrubbing below the full print, so only the current layer is shown at full brightness.
// Dim previous layers (ORCA)
// Whether the layers the layer slider is not scrubbed to are rendered darkened while
// showing less than the full print, so only the inspected layer(s) are at full brightness.
// How bright those darkened layers are rendered, 1.0 = unchanged, 0.0 = black.
//
bool is_dim_previous_layers() const;
void set_dim_previous_layers(bool value);
float get_dim_previous_layers_brightness() const;
void set_dim_previous_layers_brightness(float value);
//
// Returns true if the given option is visible.
//
+4 -2
View File
@@ -19,9 +19,11 @@ struct Settings
EViewType view_type{ EViewType::FeatureType };
ETimeMode time_mode{ ETimeMode::Normal };
bool top_layer_only_view_range{ false };
// ORCA: when enabled, all layers below the current top layer are rendered
// darkened (keeping their color) while scrubbing below the full print (ported from preFlight)
// ORCA: when enabled, every layer the layer slider is not scrubbed to is rendered
// darkened (keeping its color) while showing less than the full print
bool dim_previous_layers{ false };
// ORCA: how bright those darkened layers are rendered, 1.0 = unchanged, 0.0 = black
float dim_previous_layers_brightness{ 0.4f };
bool spiral_vase_mode{ false };
//
// Required update flags
+10
View File
@@ -82,6 +82,16 @@ void Viewer::set_dim_previous_layers(bool value)
m_impl->set_dim_previous_layers(value);
}
float Viewer::get_dim_previous_layers_brightness() const
{
return m_impl->get_dim_previous_layers_brightness();
}
void Viewer::set_dim_previous_layers_brightness(float value)
{
m_impl->set_dim_previous_layers_brightness(value);
}
bool Viewer::is_option_visible(EOptionType type) const
{
return m_impl->is_option_visible(type);
+38 -23
View File
@@ -1223,16 +1223,12 @@ static float encode_color(const Color& color) {
return static_cast<float>(i_color);
}
// ORCA: how much the layers below the current top layer are darkened when
// Settings::dim_previous_layers is enabled (ported from preFlight). 0.0 = no change, 1.0 = black.
static constexpr float PREVIOUS_LAYER_DARKEN_FACTOR = 0.60f;
// ORCA: returns the encoded color scaled towards black by 'factor', preserving its hue
static float encode_color_darkened(const Color& color, float factor) {
const float keep = 1.0f - factor;
const int r = static_cast<int>(color[0] * keep);
const int g = static_cast<int>(color[1] * keep);
const int b = static_cast<int>(color[2] * keep);
// ORCA: returns the encoded color scaled towards black by 'brightness', preserving its hue.
// 1.0 = no change, 0.0 = black.
static float encode_color_dimmed(const Color& color, float brightness) {
const int r = static_cast<int>(color[0] * brightness);
const int g = static_cast<int>(color[1] * brightness);
const int b = static_cast<int>(color[2] * brightness);
const int i_color = r << 16 | g << 8 | b;
return static_cast<float>(i_color);
}
@@ -1248,15 +1244,20 @@ void ViewerImpl::update_colors_texture()
const size_t top_layer_id = m_settings.top_layer_only_view_range ? m_layers.get_view_range()[1] : 0;
const bool color_top_layer_only = m_view_range.get_full()[1] != m_view_range.get_visible()[1];
// ORCA: when dim_previous_layers is enabled, darken every layer below the current top layer
// (keeping its color) whenever we are not rendering the whole print, so that only the layer
// being scrubbed to is shown at full brightness (ported from preFlight). This shares
// top_layer_id with the greying path, so it only applies while in top-layer-only mode - that
// way the moves slider still animates normally across all layers when that mode is disabled.
const bool dim_previous_layers = m_settings.dim_previous_layers && !m_layers.empty();
const bool full_render = (m_layers.get_view_range()[0] == 0) &&
(m_layers.get_view_range()[1] >= static_cast<uint32_t>(m_layers.count()) - 1) &&
(m_view_range.get_visible()[1] == m_view_range.get_full()[1]);
// ORCA: when dim_previous_layers is enabled, darken every layer (keeping its color) except the
// one(s) the layer slider is being scrubbed to, so that only those are shown at full brightness.
// A slider thumb marks a layer as inspected only once it is moved away from
// its end of the print: the upper one while it is below the last layer (or while the moves
// slider is not at the end of the layer), the lower one while it is above the first layer, so
// trimming the print from the bottom lights up the lowest visible layer and using the slider as
// a range lights up both ends. When neither thumb is moved the whole print is rendered normally.
// Gated on top-layer-only mode, which the greying path below also keys off of, so that the moves
// slider still animates normally across all layers when that mode is disabled.
const Interval& layers_range = m_layers.get_view_range();
const bool inspecting_top_layer = layers_range[1] + 1 < m_layers.count() || color_top_layer_only;
const bool inspecting_bottom_layer = layers_range[0] > 0;
const bool dim_previous_layers = m_settings.dim_previous_layers && m_settings.top_layer_only_view_range &&
!m_layers.empty() && (inspecting_top_layer || inspecting_bottom_layer);
// Based on current settings and slider position, we might want to render some
// vertices as dark grey (or darkened, see above). Use either that or the normal color (from the cache).
@@ -1265,9 +1266,13 @@ void ViewerImpl::update_colors_texture()
for (size_t i=0; i<m_vertices.size(); ++i) {
const PathVertex& v = m_vertices[i];
const bool keep_spiral_seam = m_settings.spiral_vase_mode && i == m_view_range.get_enabled()[0];
if (dim_previous_layers && !full_render && v.layer_id < top_layer_id && !keep_spiral_seam)
colors[i] = encode_color_darkened(get_vertex_color(v), PREVIOUS_LAYER_DARKEN_FACTOR);
else if (color_top_layer_only && v.layer_id < top_layer_id && !keep_spiral_seam)
// ORCA: layers kept at full brightness by the dimming above are excluded from the greying below too
const bool inspected_layer = dim_previous_layers &&
((inspecting_top_layer && v.layer_id == layers_range[1]) ||
(inspecting_bottom_layer && v.layer_id == layers_range[0]));
if (dim_previous_layers && !inspected_layer && !keep_spiral_seam)
colors[i] = encode_color_dimmed(get_vertex_color(v), m_settings.dim_previous_layers_brightness);
else if (!inspected_layer && color_top_layer_only && v.layer_id < top_layer_id && !keep_spiral_seam)
colors[i] = encode_color(DUMMY_COLOR);
else
colors[i] = m_vertices_colors[i];
@@ -1379,7 +1384,7 @@ void ViewerImpl::toggle_top_layer_only_view_range()
update_colors_texture();
}
// ORCA: enable/disable darkening of the layers below the current top layer (ported from preFlight)
// ORCA: enable/disable darkening of the layers the layer slider is not scrubbed to
void ViewerImpl::set_dim_previous_layers(bool value)
{
if (m_settings.dim_previous_layers == value)
@@ -1390,6 +1395,16 @@ void ViewerImpl::set_dim_previous_layers(bool value)
m_settings.update_colors = true;
}
// ORCA: set how bright the darkened layers are rendered, 1.0 = unchanged, 0.0 = black
void ViewerImpl::set_dim_previous_layers_brightness(float value)
{
value = std::clamp(value, 0.0f, 1.0f);
if (m_settings.dim_previous_layers_brightness == value)
return;
m_settings.dim_previous_layers_brightness = value;
m_settings.update_colors = true;
}
std::vector<ETimeMode> ViewerImpl::get_time_modes() const
{
std::vector<ETimeMode> ret;
+6 -1
View File
@@ -85,9 +85,14 @@ public:
bool is_top_layer_only_view_range() const { return m_settings.top_layer_only_view_range; }
void toggle_top_layer_only_view_range();
// ORCA: darken layers below the current top layer while scrubbing (ported from preFlight)
// ORCA: darken every layer the layer slider is not scrubbed to, so that only the inspected
// one(s) - the top thumb's layer, the bottom thumb's layer, or both - stay at full
// brightness. dim_previous_layers_brightness sets how dark the rest go, 1.0 = unchanged,
// 0.0 = black
bool is_dim_previous_layers() const { return m_settings.dim_previous_layers; }
void set_dim_previous_layers(bool value);
float get_dim_previous_layers_brightness() const { return m_settings.dim_previous_layers_brightness; }
void set_dim_previous_layers_brightness(float value);
bool is_spiral_vase_mode() const { return m_settings.spiral_vase_mode; }
+5
View File
@@ -754,6 +754,11 @@ set(SLIC3R_GUI_SOURCES
Utils/WxFontUtils.hpp
Utils/FileTransferUtils.cpp
Utils/FileTransferUtils.hpp
Utils/wxInspectorPlugins/DPIAwarePlugin.hpp
Utils/wxInspectorPlugins/DPIAwarePlugin.cpp
Utils/wxInspectorPlugins/CustomWidgetsPlugin.hpp
Utils/wxInspectorPlugins/CustomWidgetsPlugin.cpp
Utils/wxInspectorPlugins/Registration.hpp
)
add_subdirectory(GUI/DeviceCore)
+3 -3
View File
@@ -192,7 +192,7 @@ PingCodeBindDialog::PingCodeBindDialog(Plater* plater /*= nullptr*/)
SetSizer(sizer_main);
SetSizerAndFit(sizer_main);
Layout();
Fit();
@@ -670,7 +670,7 @@ PingCodeBindDialog::~PingCodeBindDialog() {
m_sizer_main->Add(m_sw_bind_failed_info, 0, wxALIGN_CENTER, 0);
m_sizer_main->Add(m_simplebook, 0, wxALIGN_RIGHT | wxRIGHT | wxBOTTOM, ButtonProps::ChoiceButtonGap());
SetSizer(m_sizer_main);
SetSizerAndFit(m_sizer_main);
Layout();
Fit();
Centre(wxBOTH);
@@ -992,7 +992,7 @@ UnBindMachineDialog::UnBindMachineDialog(Plater *plater /*= nullptr*/)
m_sizer_main->Add(m_sizer_button, 0, wxALIGN_RIGHT | wxRIGHT, ButtonProps::ChoiceButtonGap());
m_sizer_main->Add(0, 0, 0, wxTOP, FromDIP(20));
SetSizer(m_sizer_main);
SetSizerAndFit(m_sizer_main);
Layout();
Fit();
Centre(wxBOTH);
+2 -4
View File
@@ -632,9 +632,8 @@ EditCalibrationHistoryDialog::EditCalibrationHistoryDialog(wxWindow
main_sizer->Add(top_panel, 1, wxEXPAND | wxALL, FromDIP(20));
SetSizer(main_sizer);
SetSizerAndFit(main_sizer);
Layout();
Fit();
CenterOnParent();
wxGetApp().UpdateDlgDarkUI(this);
@@ -910,9 +909,8 @@ NewCalibrationHistoryDialog::NewCalibrationHistoryDialog(wxWindow *parent, const
main_sizer->Add(top_panel, 1, wxEXPAND | wxALL, FromDIP(20));
SetSizer(main_sizer);
SetSizerAndFit(main_sizer);
Layout();
Fit();
CenterOnParent();
wxGetApp().UpdateDlgDarkUI(this);
+1 -1
View File
@@ -162,7 +162,7 @@ CalibrationDialog::CalibrationDialog(Plater *plater)
body_panel->Layout();
m_sizer_main->Add(body_panel, 0, wxEXPAND | wxALL, FromDIP(25));
SetSizer(m_sizer_main);
SetSizerAndFit(m_sizer_main);
Layout();
Fit();
+1 -2
View File
@@ -112,9 +112,8 @@ CloneDialog::CloneDialog(wxWindow *parent)
v_sizer->Add(bottom_sizer, 0, wxEXPAND);
this->SetSizer(v_sizer);
this->SetSizerAndFit(v_sizer);
this->Layout();
v_sizer->Fit(this);
wxGetApp().UpdateDlgDarkUI(this);
+10 -5
View File
@@ -703,12 +703,13 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
toggle_line("spiral_mode_max_xy_smoothing", has_spiral_vase && config->opt_bool("spiral_mode_smooth"));
toggle_line("spiral_starting_flow_ratio", has_spiral_vase);
toggle_line("spiral_finishing_flow_ratio", has_spiral_vase);
bool has_top_shell = config->opt_int("top_shell_layers") > 0 || (has_spiral_vase && config->opt_int("bottom_shell_layers") > 1);
bool has_top_shell_layers = config->opt_int("top_shell_layers") > 0 || (has_spiral_vase && config->opt_int("bottom_shell_layers") > 1);
bool has_top_shell = has_top_shell_layers && config->option<ConfigOptionPercent>("top_surface_density")->value > 0;
bool has_bottom_shell = config->opt_int("bottom_shell_layers") > 0;
bool has_solid_infill = has_top_shell || has_bottom_shell;
bool has_solid_infill = has_top_shell_layers || has_bottom_shell;
toggle_field("top_surface_pattern", has_top_shell);
toggle_field("bottom_surface_pattern", has_bottom_shell);
toggle_field("top_surface_density", has_top_shell);
toggle_field("top_surface_density", has_top_shell_layers);
toggle_field("bottom_surface_density", has_bottom_shell);
toggle_field("top_layer_direction", has_top_shell);
toggle_field("bottom_layer_direction", has_bottom_shell);
@@ -751,7 +752,7 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
for (auto el : { "sparse_infill_speed", "bridge_speed", "internal_bridge_speed"})
toggle_field(el, have_infill || has_solid_infill, variant_index);
toggle_field("top_shell_thickness", ! has_spiral_vase && has_top_shell);
toggle_field("top_shell_thickness", ! has_spiral_vase && has_top_shell_layers);
toggle_field("bottom_shell_thickness", ! has_spiral_vase && has_bottom_shell);
// Gap fill is newly allowed in between perimeter lines even for empty infill (see GH #1476).
@@ -1008,7 +1009,11 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
toggle_line("make_overhang_printable_angle", have_make_overhang_printable);
toggle_line("make_overhang_printable_hole_size", have_make_overhang_printable);
toggle_line("min_width_top_surface", config->opt_bool("only_one_wall_top") || ((config->opt_float("min_length_factor") > 0.5f) && have_arachne)); // 0.5 is default value
// Orca: the one-wall options act on top/bottom surfaces, which exist only with a shell. An unfilled surface
// (0% surface density) is still a surface, so these are gated on the layer counts alone.
toggle_line("only_one_wall_first_layer", has_bottom_shell);
toggle_line("only_one_wall_top", has_top_shell_layers);
toggle_line("min_width_top_surface", (has_top_shell_layers && config->opt_bool("only_one_wall_top")) || ((config->opt_float("min_length_factor") > 0.5f) && have_arachne)); // 0.5 is default value
for (auto el : { "hole_to_polyhole_threshold", "hole_to_polyhole_twisted", "hole_to_polyhole_max_edges" })
toggle_line(el, config->opt_bool("hole_to_polyhole"));
+1 -2
View File
@@ -80,9 +80,8 @@ ConnectPrinterDialog::ConnectPrinterDialog(wxWindow *parent, wxWindowID id, cons
main_sizer->Add(sizer_top);
this->SetSizer(main_sizer);
this->SetSizerAndFit(main_sizer);
this->Layout();
this->Fit();
CentreOnParent();
m_textCtrl_code->Bind(wxEVT_TEXT, &ConnectPrinterDialog::on_input_enter, this);
+1
View File
@@ -13,6 +13,7 @@
#include <boost/log/trivial.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/format.hpp>
#include "nlohmann/json.hpp"
+1 -2
View File
@@ -112,9 +112,8 @@ DownloadProgressDialog::DownloadProgressDialog(wxString title)
m_simplebook_status->AddPage(m_panel_download_failed, wxEmptyString, false);
m_simplebook_status->AddPage(m_panel_install_failed, wxEmptyString, false);
SetSizer(m_sizer_main);
SetSizerAndFit(m_sizer_main);
Layout();
Fit();
CentreOnParent();
Bind(wxEVT_CLOSE_WINDOW, &DownloadProgressDialog::on_close, this);
+1 -2
View File
@@ -261,7 +261,7 @@ void ExtrusionCalibration::create()
top_sizer->Add(FromDIP(24), 0);
top_sizer->Add(sizer_main, 1, wxEXPAND);
top_sizer->Add(FromDIP(24), 0);
SetSizer(top_sizer);
SetSizerAndFit(top_sizer);
// set default nozzle
m_comboBox_nozzle_dia->SetSelection(1);
@@ -271,7 +271,6 @@ void ExtrusionCalibration::create()
set_step(1);
Layout();
Fit();
m_k_val->GetTextCtrl()->Bind(wxEVT_TEXT_ENTER, [this](wxCommandEvent& e) {
input_value_finish();
+5 -21
View File
@@ -1334,27 +1334,7 @@ void SpinCtrl::BUILD() {
if (!parsed || value < INT_MIN || value > INT_MAX)
tmp_value = UNDEF_VALUE;
else {
tmp_value = std::min(std::max((int)value, temp->GetMin()), temp->GetMax());
#ifdef __WXOSX__
#ifdef UNDEFINED__WXOSX__ // BBS
// Forcibly set the input value for SpinControl, since the value
// inserted from the keyboard or clipboard is not updated under OSX
SpinInput* spin = static_cast<SpinInput*>(window);
spin->SetValue(tmp_value);
// But in SetValue() is executed m_text_ctrl->SelectAll(), so
// discard this selection and set insertion point to the end of string
// temp->GetText()->SetInsertionPointEnd();
#endif
#else
// update value for the control only if it was changed in respect to the Min/max values
if (tmp_value != (int)value) {
temp->SetValue(tmp_value);
// But after SetValue() cursor ison the first position
// so put it to the end of string
// int pos = std::to_string(tmp_value).length();
// temp->SetSelection(pos, pos);
}
#endif
tmp_value = (int)value;
}
}), temp->GetTextCtrl()->GetId());
@@ -1375,6 +1355,10 @@ void SpinCtrl::propagate_value()
on_kill_focus();
} else {
auto ctrl = dynamic_cast<SpinInput *>(window);
tmp_value = std::min(std::max(tmp_value, ctrl->GetMin()), ctrl->GetMax());
if (ctrl->GetValue() != tmp_value)
ctrl->SetValue(tmp_value); // Clamp now when the user is done typing (kill focus / Enter / spin arrows)
if (m_value.empty()
? !ctrl->GetTextCtrl()->GetLabel().IsEmpty()
: ctrl->GetValue() != boost::any_cast<int>(m_value))
+1 -2
View File
@@ -105,9 +105,8 @@ FilamentPickerDialog::FilamentPickerDialog(wxWindow *parent, const wxString& fil
container_sizer->Add(main_sizer, 1, wxEXPAND | wxALL, FromDIP(10));
container_sizer->Add(dlg_btns, 0, wxEXPAND);
SetSizer(container_sizer);
SetSizerAndFit(container_sizer);
Layout();
container_sizer->Fit(this);
// Position the dialog relative to the parent window
if (GetParent()) {
+2 -1
View File
@@ -1141,8 +1141,9 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const
if (current_top_layer_only != required_top_layer_only)
m_viewer.toggle_top_layer_only_view_range();
// ORCA: darken layers below the current one while scrubbing the preview (ported from preFlight)
// ORCA: darken the layers the preview layer slider is not scrubbed to
m_viewer.set_dim_previous_layers(get_app_config()->get_bool("preview_dim_previous_layers"));
m_viewer.set_dim_previous_layers_brightness(0.01f * std::stoi(get_app_config()->get("preview_dim_previous_layers_brightness")));
// avoid processing if called with the same gcode_result
if (m_last_result_id == gcode_result.id && wxGetApp().is_editor()) {
+4 -1
View File
@@ -338,9 +338,12 @@ public:
libvgcode::EViewType get_view_type() const { return m_viewer.get_view_type(); }
// ORCA: darken layers below the current top layer while scrubbing the preview (ported from preFlight)
// ORCA: darken the layers not scrubbed to while using the preview layer slider
void set_dim_previous_layers(bool value) { m_viewer.set_dim_previous_layers(value); }
bool is_dim_previous_layers() const { return m_viewer.is_dim_previous_layers(); }
// ORCA: brightness of those darkened layers, 1.0 = unchanged, 0.0 = black
void set_dim_previous_layers_brightness(float value) { m_viewer.set_dim_previous_layers_brightness(value); }
float get_dim_previous_layers_brightness() const { return m_viewer.get_dim_previous_layers_brightness(); }
void set_layers_z_range(const std::array<unsigned int, 2>& layers_z_range);
+3 -3
View File
@@ -6078,10 +6078,10 @@ void GLCanvas3D::_render_3d_navigator()
strcpy(style.AxisLabels[ImGuizmo::Axis::Axis_X], "Y"); // ORCA use uppercase to match text on tranform widgets
strcpy(style.AxisLabels[ImGuizmo::Axis::Axis_Y], "Z"); // ORCA use uppercase to match text on tranform widgets
strcpy(style.AxisLabels[ImGuizmo::Axis::Axis_Z], "X"); // ORCA use uppercase to match text on tranform widgets
strcpy(style.FaceLabels[ImGuizmo::FACES::FACE_FRONT], _utf8("Front").c_str());
strcpy(style.FaceLabels[ImGuizmo::FACES::FACE_FRONT], _u8L_CONTEXT("Front", "Camera View").c_str());
strcpy(style.FaceLabels[ImGuizmo::FACES::FACE_BACK], _u8L_CONTEXT("Back", "Camera View").c_str());
strcpy(style.FaceLabels[ImGuizmo::FACES::FACE_TOP], _utf8("Top").c_str());
strcpy(style.FaceLabels[ImGuizmo::FACES::FACE_BOTTOM], _utf8("Bottom").c_str());
strcpy(style.FaceLabels[ImGuizmo::FACES::FACE_TOP], _u8L_CONTEXT("Top", "Camera View").c_str());
strcpy(style.FaceLabels[ImGuizmo::FACES::FACE_BOTTOM], _u8L_CONTEXT("Bottom", "Camera View").c_str());
strcpy(style.FaceLabels[ImGuizmo::FACES::FACE_LEFT], _u8L_CONTEXT("Left", "Camera View").c_str());
strcpy(style.FaceLabels[ImGuizmo::FACES::FACE_RIGHT], _u8L_CONTEXT("Right", "Camera View").c_str());
+18 -7
View File
@@ -96,6 +96,7 @@
#include "../Utils/PresetUpdater.hpp"
#include "../Utils/PrintHost.hpp"
#include "../Utils/Process.hpp"
#include "../Utils/wxInspectorPlugins/Registration.hpp"
#include "../Utils/MacDarkMode.hpp"
#include "../Utils/Http.hpp"
#include "../Utils/InstanceID.hpp"
@@ -2835,6 +2836,9 @@ bool GUI_App::on_init_inner()
::Label::initSysFont();
// Register wxInspector plugins for Orca custom controls
RegisterOrcaInspectorPlugins();
// Set initialization of image handlers before any UI actions - See GH issue #7469
wxInitAllImageHandlers();
#ifdef NDEBUG
@@ -3704,13 +3708,13 @@ bool GUI_App::on_init_network(bool try_backup)
}
} else {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, load dll failed";
// A failed install can leave the config naming a build that never made it to
// disk (download_plugin() adopts the downloaded version up front so that
// install_plugin() can name the library after it). If the whitelisted latest
// is still installed, fall back to it instead of dropping the user into the
// re-download flow without networking.
// A failed install can leave the config naming a build that never made it to disk;
// fall back to the installed latest instead of dropping the user into the re-download
// flow. Only when the configured library is genuinely absent, though - a pinned series
// that is on disk but failed to load once must keep its pin, not be rewritten for good.
std::string latest = get_latest_network_version();
if (config_version != latest && BBLNetworkPlugin::versioned_library_exists(latest)) {
if (config_version != latest && !BBLNetworkPlugin::versioned_library_exists(config_version)
&& BBLNetworkPlugin::versioned_library_exists(latest)) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": falling back to installed " << latest;
config_version = latest;
app_config->set_network_plugin_version(latest);
@@ -3814,7 +3818,14 @@ bool GUI_App::on_init_network(bool try_backup)
m_user_manager = new Slic3r::UserManager();
}
if (should_load_networking_plugin && m_networking_compatible && !use_legacy_network_plugin()) {
// A version pinned to something other than the latest series is a deliberate choice, so it
// is exempt from the upgrade prompt the same way the legacy pin already is - otherwise the
// dialog reappears on every launch for as long as the pin is held.
const std::string pinned_version = app_config->get_network_plugin_version();
const bool pinned_to_older_series = !pinned_version.empty() &&
network_plugin_series(pinned_version) != network_plugin_series(get_latest_network_version());
if (should_load_networking_plugin && m_networking_compatible && !pinned_to_older_series) {
app_config->clear_remind_network_update_later();
if (has_network_update_available()) {
+8 -1
View File
@@ -20,6 +20,7 @@
#include <wx/settings.h>
#include <wx/dataview.h>
#include <wx/statbox.h>
#include <wx/inspector/inspector.h>
#include <chrono>
#include "Event.hpp"
@@ -88,7 +89,7 @@ void update_dark_ui(wxWindow* window);
extern std::deque<wxDialog*> dialogStack;
template<class P> class DPIAware : public P
template<class P> class DPIAware : public P, public wxInspector::wxInspectable
{
public:
DPIAware(wxWindow *parent, wxWindowID id, const wxString &title, const wxPoint &pos=wxDefaultPosition,
@@ -107,6 +108,7 @@ public:
this->SetFont(m_normal_font);
#endif
this->CenterOnParent();
SetupInspectorAccelerator(this);
#ifdef _WIN32
update_dark_ui(this);
#endif
@@ -182,6 +184,11 @@ public:
float scale_factor() const { return m_scale_factor; }
float prev_scale_factor() const { return m_prev_scale_factor; }
// Only meant to be used by inspector, not public API
void set_scale_factor(float v) { m_scale_factor = v; }
void set_prev_scale_factor(float v) { m_prev_scale_factor = v; }
void set_em_unit(int v) { m_em_unit = v; }
bool force_rescale() const { return m_force_rescale; }
int em_unit() const { return m_em_unit; }
// int font_size() const { return m_font_size; }
+1 -1
View File
@@ -3502,7 +3502,7 @@ static void check_objects_after_cut(const ModelObjectPtrs& objects)
names += ", " + from_u8(err_objects_names[i]);
WarningDialog(wxGetApp().plater(), format_wxstr(_L("Objects(%1%) have duplicated connectors. "
"Some connectors may be missing in slicing result.\n"
"Please report to PrusaSlicer team in which scenario this issue happened.\n"
"Please report to the OrcaSlicer team in which scenario this issue happened.\n"
"Thank you."), names)).ShowModal();
}
+1 -1
View File
@@ -586,7 +586,7 @@ void ArrangeJob::process(Ctl &ctl)
params.stopcondition = [&ctl]() { return ctl.was_canceled(); };
params.progressind = [this, &ctl](unsigned num_finished, std::string str = "") {
ctl.update_status(num_finished * 100 / status_range(), _u8L("Arranging") + str);
ctl.update_status(num_finished * 100 / status_range(), _u8L("Arranging ") + str);
};
{
+5 -5
View File
@@ -739,7 +739,7 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_
return;
}
if (evt.CmdDown() && evt.GetKeyCode() == 'I') {
if (evt.CmdDown() && evt.GetKeyCode() == 'I' && !evt.ShiftDown()) {
if (!can_add_models()) return;
if (m_plater) { m_plater->add_file(); }
return;
@@ -2654,14 +2654,14 @@ static void add_common_view_menu_items(wxMenu* view_menu, MainFrame* mainFrame,
"", nullptr, [can_change_view]() { return can_change_view(); }, mainFrame);
//view_menu->AppendSeparator();
//TRN To be shown in the main menu View->Top
append_menu_item(view_menu, wxID_ANY, _L("Top") + "\t" + ctrl + "1", _L("Top View"), [mainFrame](wxCommandEvent&) { mainFrame->select_view("top"); },
append_menu_item(view_menu, wxID_ANY, _L_CONTEXT("Top", "Camera View") + "\t" + ctrl + "1", _L("Top View"), [mainFrame](wxCommandEvent&) { mainFrame->select_view("top"); },
"", nullptr, [can_change_view]() { return can_change_view(); }, mainFrame);
//TRN To be shown in the main menu View->Bottom
append_menu_item(view_menu, wxID_ANY, _L("Bottom") + "\t" + ctrl + "2", _L("Bottom View"), [mainFrame](wxCommandEvent&) { mainFrame->select_view("bottom"); },
append_menu_item(view_menu, wxID_ANY, _L_CONTEXT("Bottom", "Camera View") + "\t" + ctrl + "2", _L("Bottom View"), [mainFrame](wxCommandEvent&) { mainFrame->select_view("bottom"); },
"", nullptr, [can_change_view]() { return can_change_view(); }, mainFrame);
append_menu_item(view_menu, wxID_ANY, _L("Front") + "\t" + ctrl + "3", _L("Front View"), [mainFrame](wxCommandEvent&) { mainFrame->select_view("front"); },
append_menu_item(view_menu, wxID_ANY, _L_CONTEXT("Front", "Camera View") + "\t" + ctrl + "3", _L("Front View"), [mainFrame](wxCommandEvent&) { mainFrame->select_view("front"); },
"", nullptr, [can_change_view]() { return can_change_view(); }, mainFrame);
append_menu_item(view_menu, wxID_ANY, _L("Rear") + "\t" + ctrl + "4", _L("Rear View"), [mainFrame](wxCommandEvent&) { mainFrame->select_view("rear"); },
append_menu_item(view_menu, wxID_ANY, _L_CONTEXT("Rear", "Camera View") + "\t" + ctrl + "4", _L("Rear View"), [mainFrame](wxCommandEvent&) { mainFrame->select_view("rear"); },
"", nullptr, [can_change_view]() { return can_change_view(); }, mainFrame);
append_menu_item(view_menu, wxID_ANY, _L_CONTEXT("Left", "Camera View") + "\t" + ctrl + "5", _L("Left View"),[mainFrame](wxCommandEvent &) {mainFrame->select_view("left"); },
"", nullptr, [can_change_view]() { return can_change_view(); }, mainFrame);
+5 -5
View File
@@ -65,7 +65,7 @@ MsgDialog::MsgDialog(wxWindow *parent, const wxString &title, const wxString &he
main_sizer->Add(btn_sizer, 0, wxBOTTOM | wxRIGHT | wxEXPAND | wxTOP, FromDIP(10));
apply_style(style);
SetSizerAndFit(main_sizer);
SetSizer(main_sizer);
wxGetApp().UpdateDlgDarkUI(this);
}
@@ -221,6 +221,7 @@ void MsgDialog::apply_style(long style)
void MsgDialog::finalize()
{
GetSizer()->SetSizeHints(this);
Layout();
Fit();
CenterOnParent();
@@ -547,7 +548,7 @@ DeleteConfirmDialog::DeleteConfirmDialog(wxWindow *parent, const wxString &title
m_del_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent &e) { EndModal(wxID_OK); });
m_cancel_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent &e) { EndModal(wxID_CANCEL); });
SetSizer(m_main_sizer);
SetSizerAndFit(m_main_sizer);
Layout();
Fit();
wxGetApp().UpdateDlgDarkUI(this);
@@ -582,7 +583,7 @@ Newer3mfVersionDialog::Newer3mfVersionDialog(wxWindow *parent, const Semver *fil
main_sizer->Add(content_sizer, 0, wxEXPAND | wxALL, FromDIP(5));
main_sizer->Add(get_btn_sizer(), 0, wxEXPAND | wxALL, FromDIP(5));
this->SetSizer(main_sizer);
this->SetSizerAndFit(main_sizer);
Layout();
Fit();
wxGetApp().UpdateDlgDarkUI(this);
@@ -745,9 +746,8 @@ NetworkErrorDialog::NetworkErrorDialog(wxWindow* parent)
sizer_main->Add(sizer_button, 1, wxEXPAND | wxLEFT | wxRIGHT, 15);
sizer_main->Add(0, 0, 0, wxTOP, 18);
SetSizer(sizer_main);
SetSizerAndFit(sizer_main);
Layout();
sizer_main->Fit(this);
Centre(wxBOTH);
}
+1
View File
@@ -47,6 +47,7 @@ NetworkPluginDownloadDialog::NetworkPluginDownloadDialog(wxWindow* parent, Mode
} else {
create_missing_plugin_ui();
}
main_sizer->SetSizeHints(this);
Layout();
Fit();
CentreOnParent();
+1 -1
View File
@@ -47,7 +47,7 @@ NetworkTestDialog::NetworkTestDialog(wxWindow* parent, wxWindowID id, const wxSt
init_bind();
this->SetSizer(main_sizer);
this->SetSizerAndFit(main_sizer);
this->Layout();
this->Centre(wxBOTH);
+1 -2
View File
@@ -270,7 +270,7 @@ PartSkipDialog::PartSkipDialog(wxWindow *parent) : DPIDialog(parent, wxID_ANY, _
m_simplebook->AddPage(m_book_third_panel, _("dialog page"), false);
m_sizer->Add(m_simplebook, 1, wxEXPAND | wxALL, 5);
SetSizer(m_sizer);
SetSizerAndFit(m_sizer);
m_zoom_in_btn->Bind(wxEVT_BUTTON, &PartSkipDialog::OnZoomIn, this);
m_zoom_out_btn->Bind(wxEVT_BUTTON, &PartSkipDialog::OnZoomOut, this);
m_switch_drag_btn->Bind(wxEVT_BUTTON, &PartSkipDialog::OnSwitchDrag, this);
@@ -281,7 +281,6 @@ PartSkipDialog::PartSkipDialog(wxWindow *parent) : DPIDialog(parent, wxID_ANY, _
m_all_checkbox->Bind(wxEVT_TOGGLEBUTTON, &PartSkipDialog::OnAllCheckbox, this);
Layout();
Fit();
CentreOnParent();
}
+52 -12
View File
@@ -14400,7 +14400,7 @@ void Plater::calib_temp(const Calib_Params& params) {
}
}
if (std::abs(nozzle_scale - 1.0) > EPSILON)
if (params.nozzle_based_resize && std::abs(nozzle_scale - 1.0) > EPSILON)
model().objects[0]->scale(nozzle_scale, nozzle_scale, nozzle_scale);
model().objects[0]->ensure_on_bed();
@@ -14408,7 +14408,9 @@ void Plater::calib_temp(const Calib_Params& params) {
printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false});
set_config_values<int, ConfigOptionInts>(filament_config, "nozzle_temperature_initial_layer", (int) start_temp);
set_config_values<int, ConfigOptionInts>(filament_config, "nozzle_temperature", (int) start_temp);
model().objects[0]->config.set_key_value("layer_height", new ConfigOptionFloat(nozzle_diameter/2));
// When resizing is disabled the 0.4 mm / 0.2 mm reference model is printed as-is (preset layer height kept).
if (params.nozzle_based_resize)
model().objects[0]->config.set_key_value("layer_height", new ConfigOptionFloat(nozzle_diameter/2));
model().objects[0]->config.set_key_value("brim_type", new ConfigOptionEnum<BrimType>(btOuterOnly));
model().objects[0]->config.set_key_value("brim_width", new ConfigOptionFloat(5.0));
model().objects[0]->config.set_key_value("brim_object_gap", new ConfigOptionFloat(0.0));
@@ -14419,7 +14421,8 @@ void Plater::calib_temp(const Calib_Params& params) {
auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config;
print_config->set_key_value("enable_wrapping_detection", new ConfigOptionBool(false));
print_config->set_key_value("initial_layer_print_height", new ConfigOptionFloat(nozzle_diameter/2));
if (params.nozzle_based_resize)
print_config->set_key_value("initial_layer_print_height", new ConfigOptionFloat(nozzle_diameter/2));
changed_objects({ 0 });
@@ -14583,6 +14586,42 @@ void Plater::calib_VFA(const Calib_Params& params)
auto print_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config;
auto filament_config = &wxGetApp().preset_bundle->filaments.get_edited_preset().config;
auto printer_config = &wxGetApp().preset_bundle->printers.get_edited_preset().config;
const ConfigOptionFloats* nozzle_diameter_config = printer_config->option<ConfigOptionFloats>("nozzle_diameter");
size_t nozzle_id = static_cast<size_t>(std::max(params.extruder_id, 0));
double nozzle_diameter = vfa_base_nozzle_diameter;
if (nozzle_diameter_config && !nozzle_diameter_config->values.empty()) {
nozzle_id = std::min(nozzle_id, nozzle_diameter_config->values.size() - 1);
nozzle_diameter = nozzle_diameter_config->values[nozzle_id];
}
if (nozzle_diameter <= 0.0)
nozzle_diameter = vfa_base_nozzle_diameter;
// Resolved layer height: use the (possibly auto-adjusted) value from the dialog, else default to nozzle/2.
double layer_height = params.vfa_layer_height > 0.0 ? params.vfa_layer_height : nozzle_diameter / 2.0;
// cut upper (on the unscaled model, using the base block height); the scaling below keeps the physical
// block height (vfa_layers_per_block * layer_height) in sync with the speed stepping in GCode::process_layer.
// Subtract EPSILON (as the temperature tower does) so the cut lands just below the flat block surface instead
// of exactly on it, which would otherwise add a degenerate extra layer.
auto obj_bb = model().objects[0]->bounding_box_exact();
auto height = vfa_base_block_height * ((params.end - params.start) / params.step + 1) - EPSILON;
if (height < obj_bb.size().z()) {
cut_horizontal(0, 0, height, ModelObjectCutAttribute::KeepLower);
}
// When resizing is enabled, XY scales with the nozzle (footprint / line width) and Z scales so each base
// block becomes vfa_layers_per_block layers of the resolved layer height. When disabled the 0.4 mm / 0.2 mm
// reference model is printed as-is (preset layer height kept).
if (params.nozzle_based_resize) {
const double xy_scale = nozzle_diameter / vfa_base_nozzle_diameter;
const double z_scale = (vfa_layers_per_block * layer_height) / vfa_base_block_height;
if (std::abs(xy_scale - 1.0) > EPSILON || std::abs(z_scale - 1.0) > EPSILON)
model().objects[0]->scale(xy_scale, xy_scale, z_scale);
}
model().objects[0]->ensure_on_bed();
printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false});
filament_config->set_key_value("slow_down_layer_time", new ConfigOptionFloats { 0.0 });
set_config_values<bool, ConfigOptionBoolsNullable>(print_config, "enable_overhang_speed", false);
@@ -14596,6 +14635,10 @@ void Plater::calib_VFA(const Calib_Params& params)
print_config->set_key_value("spiral_mode", new ConfigOptionBool(true));
print_config->set_key_value("enable_wrapping_detection", new ConfigOptionBool(false));
print_config->set_key_value("precise_z_height", new ConfigOptionBool(false));
if (params.nozzle_based_resize) {
print_config->set_key_value("initial_layer_print_height", new ConfigOptionFloat(layer_height));
model().objects[0]->config.set_key_value("layer_height", new ConfigOptionFloat(layer_height));
}
model().objects[0]->config.set_key_value("brim_type", new ConfigOptionEnum<BrimType>(btOuterOnly));
model().objects[0]->config.set_key_value("brim_width", new ConfigOptionFloat(3.0));
model().objects[0]->config.set_key_value("brim_object_gap", new ConfigOptionFloat(0.0));
@@ -14606,14 +14649,11 @@ void Plater::calib_VFA(const Calib_Params& params)
wxGetApp().get_tab(Preset::TYPE_PRINT)->update_ui_from_settings();
wxGetApp().get_tab(Preset::TYPE_FILAMENT)->update_ui_from_settings();
// cut upper
auto obj_bb = model().objects[0]->bounding_box_exact();
auto height = 5 * ((params.end - params.start) / params.step + 1);
if (height < obj_bb.size().z()) {
cut_horizontal(0, 0, height, ModelObjectCutAttribute::KeepLower);
}
p->background_process.fff_print()->set_calib_params(params);
// Pass the resolved layer height on (only meaningful when resized). GCode's VFA stepping is layer-based, so
// it does not require it, but keep it consistent with the geometry.
Calib_Params calib_params = params;
calib_params.vfa_layer_height = params.nozzle_based_resize ? layer_height : 0.0;
p->background_process.fff_print()->set_calib_params(calib_params);
}
void Plater::calib_input_shaping_freq(const Calib_Params& params)
@@ -15310,7 +15350,7 @@ ProjectDropDialog::ProjectDropDialog(const std::string &filename)
m_sizer_main->Add(dlg_btns, 0, wxEXPAND);
SetSizer(m_sizer_main);
SetSizerAndFit(m_sizer_main);
Layout();
Fit();
Centre(wxBOTH);
+31 -1
View File
@@ -700,6 +700,12 @@ wxBoxSizer *PreferencesDialog::create_item_spinctrl(wxString title, wxString tit
auto input = new SpinInput(m_parent, wxEmptyString, side_label, wxDefaultPosition, DESIGN_INPUT_SIZE, wxSP_ARROW_KEYS, min, max, stoi(app_config->get(param)));
input->SetToolTip(tip);
// ORCA: this one is only meaningful while the dimming it controls is enabled
if (param == "preview_dim_previous_layers_brightness") {
m_dim_previous_layers_brightness_input = input;
input->Enable(app_config->get_bool("preview_dim_previous_layers"));
}
m_sizer->Add(input, 0, wxALIGN_CENTER_VERTICAL);
if(!title2.empty()){
@@ -1050,8 +1056,10 @@ wxBoxSizer *PreferencesDialog::create_item_checkbox(wxString title, wxString too
wxGetApp().mainframe->m_webview->SendCloudProvidersInfo();
}
}
// ORCA: apply the preview dimming change immediately to the currently loaded preview (ported from preFlight)
// ORCA: apply the preview dimming change immediately to the currently loaded preview
else if (param == "preview_dim_previous_layers") {
if (m_dim_previous_layers_brightness_input)
m_dim_previous_layers_brightness_input->Enable(app_config->get_bool(param));
if (Plater* plater = wxGetApp().plater()) {
if (GLCanvas3D* canvas = plater->get_preview_canvas3D()) {
canvas->get_gcode_viewer().set_dim_previous_layers(app_config->get_bool(param));
@@ -1914,6 +1922,28 @@ void PreferencesDialog::create_items()
);
g_sizer->Add(item_dim_previous_layers);
auto item_dim_previous_layers_brightness = create_item_spinctrl(
_L("Dimmed layer brightness"),
"",
_L("%"),
_L("How brightly the dimmed layers are rendered when \"Dim lower layers\" is enabled.\n"
"99% is barely darkened, 0% renders them black. Capped at 99% because 100% would be the same as disabling the option."),
"preview_dim_previous_layers_brightness",
0,
99,
// ORCA: apply the new brightness immediately to the currently loaded preview
[](int value) {
if (Plater* plater = wxGetApp().plater()) {
if (GLCanvas3D* canvas = plater->get_preview_canvas3D()) {
canvas->get_gcode_viewer().set_dim_previous_layers_brightness(0.01f * value);
canvas->set_as_dirty();
canvas->request_extra_frame();
}
}
}
);
g_sizer->Add(item_dim_previous_layers_brightness);
g_sizer->AddSpacer(FromDIP(10));
sizer_page->Add(g_sizer, 0, wxEXPAND);
+2
View File
@@ -13,6 +13,7 @@
#include "Widgets/ComboBox.hpp"
#include "Widgets/CheckBox.hpp"
#include "Widgets/TextInput.hpp"
#include "Widgets/SpinInput.hpp"
#include "Widgets/TabCtrl.hpp"
#include "slic3r/Utils/bambu_networking.hpp"
@@ -71,6 +72,7 @@ public:
::CheckBox * m_sync_user_preset_checkbox = {nullptr};
::CheckBox * m_bambu_cloud_checkbox = {nullptr};
::TextInput *m_backup_interval_textinput = {nullptr};
::SpinInput *m_dim_previous_layers_brightness_input = {nullptr};
::ComboBox * m_network_version_combo = {nullptr};
std::vector<NetworkLibraryVersionInfo> m_available_versions;
+6 -7
View File
@@ -28,7 +28,7 @@ PrintOptionsDialog::PrintOptionsDialog(wxWindow* parent)
{
this->SetDoubleBuffered(true);
SetBackgroundColour(*wxWHITE);
SetSize(FromDIP(480),FromDIP(520));
// SetMinSize(FromDIP(wxSize{wxDefaultCoord,520}));
m_scrollwindow = new wxScrolledWindow(this, wxID_ANY);
@@ -50,7 +50,8 @@ PrintOptionsDialog::PrintOptionsDialog(wxWindow* parent)
m_scrollwindow->FitInside();
this->Layout();
// mainSizer->Fit(this);
mainSizer->SetMinSize(wxDefaultCoord, FromDIP(520));
mainSizer->Fit(this);
//this->Fit();
m_cb_ai_monitoring->Bind(wxEVT_TOGGLEBUTTON, [this](wxCommandEvent &evt) {
@@ -1670,12 +1671,9 @@ PrinterPartsDialog::PrinterPartsDialog(wxWindow* parent)
/*inset data*/
sizer->Add(single_panel, 0, wxEXPAND, 0);
sizer->Add(multiple_panel, 0, wxEXPAND, 0);
SetSizer(sizer);
Layout();
Fit();
single_panel->Hide();
SetSizerAndFit(sizer);
Layout();
wxGetApp().UpdateDlgDarkUI(this);
}
@@ -1752,6 +1750,7 @@ bool PrinterPartsDialog::Show(bool show)
}
}
GetSizer()->SetSizeHints(this);
Layout();
Fit();
}
+1 -1
View File
@@ -119,7 +119,7 @@ PublishDialog::PublishDialog(Plater *plater)
top_sizer->Add(m_main_sizer, 1, wxALL | wxEXPAND, 0);
top_sizer->Add(FromDIP(30), 0, 0, wxEXPAND, 0);
this->SetSizer(top_sizer);
this->SetSizerAndFit(top_sizer);
this->Layout();
this->Centre(wxBOTH);
+1 -2
View File
@@ -310,10 +310,9 @@ StepMeshDialog::StepMeshDialog(wxWindow* parent, Slic3r::Step& file, double line
bSizer->Add(bSizer_button, 1, wxEXPAND);
this->SetSizer(bSizer);
this->SetSizerAndFit(bSizer);
update_mesh_number_text();
this->Layout();
bSizer->Fit(this);
this->Bind(wxEVT_LEFT_DOWN, [this](auto& e) {
SetFocusIgnoringChildren();
+1 -1
View File
@@ -354,7 +354,7 @@ TroubleshootDialog::TroubleshootDialog()
m_sizer->AddSpacer(FromDIP(20));
m_sizer->Add(right_sizer, 0, wxEXPAND | wxTOP | wxBOTTOM | wxRIGHT, FromDIP(15));
SetSizer(m_sizer);
SetSizerAndFit(m_sizer);
Layout();
Fit();
CenterOnParent();
+3
View File
@@ -168,6 +168,9 @@ private:
}
wxClientDC dc(this);
int cWidth = GetClientSize().GetWidth();
// Don't compute/commit a size based on a not-yet-laid-out width
// Mirrors the guard in OnPaint() so both use the same wrap results
if (cWidth < 50) return;
int y = 0;
for (size_t i = 0; i < m_lines.size(); ++i) {
+7 -6
View File
@@ -773,7 +773,7 @@ std::vector<std::string> DiffViewCtrl::selected_options()
static std::string none{"none"};
#define UNSAVE_CHANGE_DIALOG_SCROLL_WINDOW_SIZE wxSize(FromDIP(490), FromDIP(374))
#define UNSAVE_CHANGE_DIALOG_ACTION_LINE_SIZE wxSize(FromDIP(490), FromDIP(60))
#define UNSAVE_CHANGE_DIALOG_ACTION_LINE_SIZE wxSize(FromDIP(490), -1)
#define UNSAVE_CHANGE_DIALOG_FIRST_VALUE_WIDTH FromDIP(190)
#define UNSAVE_CHANGE_DIALOG_VALUE_WIDTH FromDIP(150)
#define UNSAVE_CHANGE_DIALOG_ITEM_HEIGHT FromDIP(24)
@@ -1075,11 +1075,6 @@ void UnsavedChangesDialog::build(Preset::Type type, PresetCollection *dependent_
m_sizer_main->Add(m_sizer_button, 0, wxEXPAND | wxTOP, 6);
m_sizer_main->Add(0, 0, 1, wxTOP, 18);
SetSizer(m_sizer_main);
Layout();
Fit();
Centre(wxBOTH);
if (params) {
if (params->left_to_right)
update_tree(type, params->config, params->from, params->to);
@@ -1095,6 +1090,11 @@ void UnsavedChangesDialog::build(Preset::Type type, PresetCollection *dependent_
//topSizer->SetSizeHints(this);
show_info_line(Action::Undef);
SetSizerAndFit(m_sizer_main);
Layout();
Fit();
// Centre(wxBOTH);
}
void UnsavedChangesDialog::show_info_line(Action action, std::string preset_name)
@@ -1499,6 +1499,7 @@ void UnsavedChangesDialog::update(Preset::Type type, PresetCollection* dependent
}
m_action_line->SetLabel(action_msg);
m_action_line->Wrap(UNSAVE_CHANGE_DIALOG_SCROLL_WINDOW_SIZE.x);
update_tree(type, presets);
update_list();
+1 -2
View File
@@ -213,9 +213,8 @@ MsgUpdateConfig::MsgUpdateConfig(const std::vector<Update> &updates, bool force_
m_scrollwindw_release_note->Layout();
SetSizer(m_sizer_main);
SetSizerAndFit(m_sizer_main);
Layout();
m_sizer_main->Fit(this);
Centre(wxBOTH);
wxGetApp().UpdateDlgDarkUI(this);
+5
View File
@@ -77,6 +77,11 @@ public:
void SetSelected(bool selected = true) { m_selected = selected; }
// Only meant to be used by inspector, not public API
ButtonStyle GetStyle() const { return m_style; }
ButtonType GetType() const { return m_type; }
bool IsSelected() const { return m_selected; }
bool Enable(bool enable = true) override;
void EnableTooltipEvenDisabled();// The tip will be shown even if the button is disabled
+3
View File
@@ -15,6 +15,9 @@ public:
void SetHalfChecked(bool value = true);
// Only meant to be used by inspector, not public API
bool IsHalfChecked() const { return m_half_checked; }
void Rescale();
#ifdef __WXOSX__
@@ -46,6 +46,12 @@ public:
bool Enable(bool enable) override;
// Only meant to be used by inspector, not public API
int GetCornerRadius() const { return m_radius; }
int GetBorderWidth() const { return m_border_width; }
StateColor GetBorderColor() const { return border_color; }
float GetScale() const { return m_scale; }
private:
void PickDC(wxDC& dc);

Some files were not shown because too many files have changed in this diff Show More