Merge branch '2.3.0' into dev_2.2.3_alves_bug_fix

# Conflicts:
#	.gitignore
#	resources/web/flutter_web/flutter_bootstrap.js
#	resources/web/flutter_web/flutter_service_worker.js
#	resources/web/flutter_web/main.dart.js
#	resources/web/flutter_web/version.changelog
#	resources/web/flutter_web/version.json
#	scripts/flatpak/io.github.Snapmaker.Snapmaker_Orca.metainfo.xml
#	src/libslic3r/GCode/WipeTower2.cpp
This commit is contained in:
alves
2026-03-10 15:33:16 +08:00
63 changed files with 118797 additions and 100503 deletions
+3 -1
View File
@@ -40,5 +40,7 @@ resources/profiles/user/default
*.code-workspace
deps_src/build/
.claude/
.omc
.hive-mind/
nul
.omc/
.claude-flow/
+152
View File
@@ -0,0 +1,152 @@
# Filament-Extruder Mapping Analysis - Final Findings
## The Key Discovery
After investigating the original implementation vs the optimized version, I found the **critical missing piece**: `initialize_filament_extruder_map()`.
## How It Actually Works
### The Initialization Process
**Called in PrintApply.cpp during apply():**
```cpp
// PrintApply.cpp line 1276
this->initialize_filament_extruder_map();
```
**Implementation in Print.cpp (lines 497-544):**
```cpp
void Print::initialize_filament_extruder_map()
{
m_filament_extruder_map.clear();
// Get the number of physical extruders
size_t physical_extruder_count = m_config.nozzle_diameter.values.size();
// Get ALL configured filaments (not just used ones)
std::vector<unsigned int> filament_extruders = this->extruders();
if (filament_extruders.empty()) {
size_t filament_count = m_config.filament_diameter.size();
for (size_t i = 0; i < filament_count; ++i) {
filament_extruders.push_back((unsigned int)i);
}
}
// Create mapping: filament_id -> physical_extruder_id
// Mapping formula: physical_extruder = filament_id % physical_extruder_count
for (unsigned int filament_idx : filament_extruders) {
int physical_extruder = filament_idx % physical_extruder_count;
m_filament_extruder_map[filament_idx] = physical_extruder;
}
}
```
### The Result
After initialization:
- Filament 0 → Extruder 0
- Filament 1 → Extruder 1
- Filament 2 → Extruder 2
- Filament 3 → Extruder 3
- Filament 4 → Extruder 0 (modulo)
- Filament 5 → Extruder 1 (modulo)
- Filament 6 → Extruder 2 (modulo)
- Filament 7 → Extruder 3 (modulo)
**The map is ALWAYS populated before slicing!**
## Why Both Versions Work
### Original Implementation (9d423d0714)
**get_physical_extruder():**
```cpp
int get_physical_extruder(int filament_idx) const {
auto it = m_filament_extruder_map.find(filament_idx);
// Fallback: identity mapping (filament 5 → extruder 5)
int physical_extruder_id = (it != m_filament_extruder_map.end()) ? it->second : filament_idx;
return physical_extruder_id;
}
```
**Why it worked:** The fallback logic was **never reached** because `initialize_filament_extruder_map()` always populates the map with modulo mapping.
### Optimized Implementation (989a53e124)
**get_physical_extruder():**
```cpp
int get_physical_extruder(int filament_idx) const {
auto it = m_filament_extruder_map.find(filament_idx);
if (it != m_filament_extruder_map.end()) {
return it->second;
} else {
// Fallback: modulo mapping (filament 5 → extruder 1)
size_t physical_count = m_config.nozzle_diameter.values.size();
return filament_idx % physical_count;
}
}
```
**Why it also works:** Same reason - the map is always populated. The fallback is just defensive programming.
## What Actually Changed
The key difference between the two versions is **NOT** the behavior during normal operation (both work the same), but:
1. **Defensive programming:** The optimized version has a safer fallback (modulo instead of identity)
2. **Code simplification:** The optimized version removed some redundant physical_extruder calculations in GCode.cpp
3. **Better logging:** The optimized version has more detailed logging
4. **Validation:** The optimized version added bounds checking in PrintApply.cpp
## The Real Issue: Why 3MF Slicing Might Fail
If the user is experiencing issues with >4 filament 3MF slicing, it's **NOT** because of the identity vs modulo fallback difference (since that code path is never reached).
Possible causes:
1. **initialize_filament_extruder_map() not being called:** Check if there's a code path that bypasses Print::apply()
2. **Config array access using filament_id instead of physical_extruder_id:** The optimized version removed some explicit physical_extruder calculations in GCode.cpp. If those changes introduced direct filament_id access to config arrays, that would cause crashes.
3. **3MF file format issues:** The 3MF file might have inconsistent filament/extruder configurations
4. **Placeholder replacement:** GCode placeholder macros might still be using filament_id instead of physical_extruder_id
## Code Changes That Matter
### Potentially Problematic Change in GCode.cpp
**Optimized version removed explicit physical_extruder calculation:**
```cpp
// BEFORE (9d423d0714):
int previous_physical_extruder = (previous_extruder_id >= 0) ?
gcode_writer.get_physical_extruder(previous_extruder_id) : -1;
int new_physical_extruder = gcode_writer.get_physical_extruder(new_extruder_id);
float old_retract_length = (gcode_writer.extruder() != nullptr && previous_physical_extruder >= 0) ?
full_config.retraction_length.get_at(previous_physical_extruder) : 0;
// AFTER (989a53e124):
float old_retract_length = (gcode_writer.extruder() != nullptr && previous_extruder_id >= 0) ?
full_config.retraction_length.get_at(previous_extruder_id) : 0; // Uses filament_id directly!
```
**This is a BUG!** The optimized version uses `previous_extruder_id` (filament_id) directly to access `retraction_length` array. This will cause array out-of-bounds access when filament_id >= physical_extruder_count.
## Recommendation
The optimized version (989a53e124) may have introduced regressions by removing explicit physical_extruder calculations. Need to:
1. **Audit all config array access** in GCode.cpp to ensure physical_extruder_id is used
2. **Add bounds checking** or use the PHYSICAL_EXTRUDER_CONFIG macro consistently
3. **Test with 8-filament 3MF** on 4-extruder configuration
## Summary
| Aspect | Original (9d423d0714) | Optimized (989a53e124) |
|--------|----------------------|------------------------|
| **Modulo mapping** | ✓ Via initialize_filament_extruder_map() | ✓ Via initialize_filament_extruder_map() |
| **Fallback logic** | Identity (never used) | Modulo (never used) |
| **Config array access** | Explicit physical_extruder calculation | Some direct filament_id access (BUG?) |
| **Safety** | Good | Potentially introduced bugs |
**Bottom line:** Both versions use the same modulo mapping via `initialize_filament_extruder_map()`, but the optimized version may have introduced bugs by simplifying config array access.
+261
View File
@@ -0,0 +1,261 @@
# Filament-Extruder Mapping: Original Implementation vs Optimized Version
## Executive Summary
This document analyzes the differences between the original working filament-extruder mapping implementation (commit `9d423d0714`) and the optimized version (commit `989a53e124`), focusing on why the original was working for >4 filament 3MF slicing.
## Key Findings
### The Critical Difference: Default Mapping Behavior
**Original Implementation (9d423d0714):**
```cpp
int get_physical_extruder(int filament_idx) const {
auto it = m_filament_extruder_map.find(filament_idx);
// When no mapping exists, use IDENTITY mapping (filament 5 → extruder 5)
int physical_extruder_id = (it != m_filament_extruder_map.end()) ? it->second : filament_idx;
return physical_extruder_id;
}
```
**Optimized Implementation (989a53e124):**
```cpp
int get_physical_extruder(int filament_idx) const {
auto it = m_filament_extruder_map.find(filament_idx);
int physical_extruder_id;
if (it != m_filament_extruder_map.end()) {
physical_extruder_id = it->second;
} else {
// When no mapping exists, use MODULO mapping (filament 5 → extruder 1)
size_t physical_count = m_config.nozzle_diameter.values.size();
if (physical_count == 0) {
physical_extruder_id = 0;
} else {
physical_extruder_id = filament_idx % physical_count;
}
}
return physical_extruder_id;
}
```
### Why the Original Worked for >4 Filaments
When slicing a 3MF file with 5-8 filaments on a 4-extruder printer:
**Original behavior (IDENTITY mapping):**
- Filament 0 → Extruder 0
- Filament 1 → Extruder 1
- Filament 2 → Extruder 2
- Filament 3 → Extruder 3
- Filament 4 → Extruder 4 (out of bounds!)
- Filament 5 → Extruder 5 (out of bounds!)
- etc.
This would cause **array out-of-bounds errors** when accessing config arrays like `nozzle_diameter[5]`, `retraction_length[5]`, etc.
**However**, the original implementation had a critical workaround: **GCode placeholder replacement still used the filament ID directly**, not the physical extruder ID.
### Placeholder Replacement Behavior
**Original (9d423d0714):**
```cpp
// GCode.cpp - toolchange_gcode placeholder replacement
// Used filament_id directly for placeholders like { filament_extruder }
std::string toolchange_gcode = this->config().toolchange_gcode;
toolchange_gcode = replace_tool_macros(
toolchange_gcode,
extruder_id, // filament_id
previous_extruder,
// ...
);
```
This meant:
- GCode T commands: T5, T6, T7, T8 (filament IDs)
- Placeholder {filament_extruder}: 5, 6, 7, 8 (filament IDs)
- **But config array access**: filament 5 → physical 5 → **CRASH**
**Wait, this doesn't add up!** If the original was crashing on config access, how could it work?
### The Real Solution: Config Array Access Pattern
The original implementation must have had additional protection. Let me verify...
Actually, looking at the original diff more carefully:
**Original Extruder constructor (9d423d0714):**
```cpp
Extruder::Extruder(const PrintConfig& config, uint16_t extruder_id)
: m_id(extruder_id)
, m_technology(config.printer_technology)
{
// CRITICAL FIX: Get physical extruder index
uint16_t physical_extruder = config.get_physical_extruder(m_id);
// Use physical extruder index for parameter access
m_nozzle_diameter = float(config.nozzle_diameter.get_at(physical_extruder));
// ... other parameters
}
```
So the original **DID** use `get_physical_extruder()` for config array access!
This means the original implementation would crash with identity mapping when accessing `nozzle_diameter[5]` on a 4-extruder printer.
### The Missing Piece: PrintApply.cpp Validation
Let me check if there was validation that prevented this scenario...
**Optimized version (989a53e124) added validation in PrintApply.cpp:**
```cpp
// Validate that all filament IDs map to valid physical extruders
for (const auto& [filament_id, physical_id] : filament_extruder_map) {
if (physical_id >= (int)nozzle_diameter.size()) {
throw ConfigurationError("Filament " + std::to_string(filament_id) +
" maps to physical extruder " + std::to_string(physical_id) +
" but only " + std::to_string(nozzle_diameter.size()) +
" extruders available");
}
}
```
### The Hypothesis: What Actually Made It Work
Given the evidence, there are two possibilities:
**Hypothesis 1: The Original Never Actually Worked**
- The original implementation (9d423d0714) was added on Feb 4, 2026
- The optimized version (989a53e124) was added on Feb 5, 2026 (only 1 day later!)
- The original may have had the identity mapping bug which was quickly fixed
- User's "working" version may have been a different branch or configuration
**Hypothesis 2: 3MF Files Include Pre-built Maps**
- 3MF files can embed filament-to-extruder mappings
- If the 3MF file had `filament_extruder_map = [0,1,2,3,0,1,2,3]` pre-configured
- Then the explicit mapping path would be used, avoiding the identity mapping
- This would work correctly without modulo
**Hypothesis 3: Empty Map = Different Behavior**
- When `m_filament_extruder_map` is empty (not set in 3MF)
- Original: Uses identity mapping → crashes
- Optimized: Uses modulo mapping → works
- User's 3MF files must have had explicit mappings
## Detailed Comparison Table
| Aspect | Original (9d423d0714) | Optimized (989a53e124) |
|--------|----------------------|------------------------|
| **Empty map behavior** | Identity: filament_id → filament_id | Modulo: filament_id % physical_count |
| **5 filaments on 4-extruder** | Filament 4 → Extruder 4 → **CRASH** | Filament 4 → Extruder 0 → ✅ Works |
| **8 filaments on 4-extruder** | Filaments 4-7 → Extruders 4-7 → **CRASH** | Filaments 4-7 → Extruders 0-3 → ✅ Works |
| **GCode T commands** | Used filament_id directly | Uses physical_extruder_id |
| **Config array access** | Used physical_extruder via get_physical_extruder() | Uses physical_extruder via get_physical_extruder() |
| **Logging** | Basic logging | Enhanced logging with map size |
| **Validation** | None | Bounds checking in PrintApply.cpp |
| **Comments** | Chinese comments | Chinese comments + English explanations |
## Code Changes Summary
### Print.hpp - get_physical_extruder()
**Before (9d423d0714):**
```cpp
int get_physical_extruder(int filament_idx) const {
auto it = m_filament_extruder_map.find(filament_idx);
int physical_extruder_id = (it != m_filament_extruder_map.end()) ? it->second : filament_idx;
BOOST_LOG_TRIVIAL(info) << "Print::get_physical_extruder: filament_id=" << filament_idx
<< " -> physical_extruder_id=" << physical_extruder_id
<< " (map_size=" << m_filament_extruder_map.size() << ")"
<< (it != m_filament_extruder_map.end() ? " [from_map]" : " [default_identity]");
return physical_extruder_id;
}
```
**After (989a53e124):**
```cpp
int get_physical_extruder(int filament_idx) const {
auto it = m_filament_extruder_map.find(filament_idx);
int physical_extruder_id;
if (it != m_filament_extruder_map.end()) {
physical_extruder_id = it->second;
} else {
size_t physical_count = m_config.nozzle_diameter.values.size();
if (physical_count == 0) {
physical_extruder_id = 0;
BOOST_LOG_TRIVIAL(warning) << "Print::get_physical_extruder: nozzle_diameter is empty! Using default physical_extruder=0";
} else {
physical_extruder_id = filament_idx % physical_count;
}
}
BOOST_LOG_TRIVIAL(info) << "Print::get_physical_extruder: filament_id=" << filament_idx
<< " -> physical_extruder_id=" << physical_extruder_id
<< " (map_size=" << m_filament_extruder_map.size() << ")"
<< (it != m_filament_extruder_map.end() ? " [from_map]" : " [default_mod]");
return physical_extruder_id;
}
```
### GCode.cpp - Simplified Access Pattern
**Before (9d423d0714):**
```cpp
// Complex physical extruder calculation everywhere
int previous_physical_extruder = (previous_extruder_id >= 0) ?
gcode_writer.get_physical_extruder(previous_extruder_id) : -1;
int new_physical_extruder = gcode_writer.get_physical_extruder(new_extruder_id);
float old_retract_length = (gcode_writer.extruder() != nullptr && previous_physical_extruder >= 0) ?
full_config.retraction_length.get_at(previous_physical_extruder) : 0;
float new_retract_length = full_config.retraction_length.get_at(new_physical_extruder);
```
**After (989a53e124):**
```cpp
// Direct filament_id access (simpler, relies on get_physical_extruder() internally)
float old_retract_length = (gcode_writer.extruder() != nullptr && previous_extruder_id >= 0) ?
full_config.retraction_length.get_at(previous_extruder_id) : 0;
float new_retract_length = full_config.retraction_length.get_at(new_extruder_id);
```
**Wait, this is WRONG!** The optimized version REMOVED the physical_extruder calculation in GCode.cpp and went back to using filament_id directly for config access. This would cause the same crash as before!
### The Critical Insight: Config Wrapper
The optimized version must have added a wrapper layer. Let me check...
Actually, looking at the diff, the optimized version added:
```cpp
#define EXTRUDER_CONFIG(OPT) m_config.OPT.get_at(m_writer.extruder()->id())
#define PHYSICAL_EXTRUDER_CONFIG(OPT) m_config.OPT.get_at(m_writer.get_physical_extruder(m_writer.extruder()->id()))
```
But I don't see widespread usage of PHYSICAL_EXTRUDER_CONFIG in the diff.
### Conclusion: The Optimization May Have Introduced a Bug
The evidence suggests:
1. **Original (9d423d0714)**: Had identity mapping bug but also had explicit physical_extruder calculations in GCode.cpp that may have prevented crashes in some paths
2. **Optimized (989a53e124)**: Fixed the identity mapping bug (good) but removed some of the explicit physical_extruder calculations (potentially bad)
3. **User's Issue**: The user says the original was working for >4 filaments. This suggests either:
- Their 3MF files had explicit `filament_extruder_map` configurations
- They were using a different code path that didn't hit the bug
- There's additional context I'm missing
## Recommendations
To properly fix this for >4 filament 3MF slicing:
1. **Keep the modulo mapping** from the optimized version - this is correct for the default case
2. **Add back physical_extruder calculations** in GCode.cpp for all config array access
3. **Add validation** to ensure filament 3MF files either have explicit mappings or work with modulo
4. **Test with actual >4 filament 3MF files** to verify the fix
## Next Steps
1. Check if user's 3MF files have explicit `filament_extruder_map` configurations
2. Verify which code paths are actually used during 3MF slicing
3. Add comprehensive bounds checking to prevent crashes
4. Test with real 8-filament 3MF files on 4-extruder configuration
File diff suppressed because it is too large Load Diff
+204
View File
@@ -0,0 +1,204 @@
; [1] PACK_SOURCE_DIR = compile-time only (e.g. .\build\Snapmaker_Orca). [2] INSTALL_DIR_RUNTIME = runtime install dir (default .\ = $EXEDIR).
!include "MUI2.nsh"
!include "FileFunc.nsh"
!include "LogicLib.nsh"
!define PRODUCT_NAME "Snapmaker Orca"
!define PRODUCT_PUBLISHER "Snapmaker"
!define PRODUCT_WEB_SITE "https://github.com/Snapmaker/OrcaSlicer"
!define PRODUCT_UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}"
!define PRODUCT_UNINST_ROOT_KEY "HKLM"
!define PRODUCT_INSTALL_KEY "Software\${PRODUCT_PUBLISHER}\${PRODUCT_NAME}"
!ifndef VERSION
!define VERSION "2.2.3"
!endif
!ifndef SOURCE_DIR
!define SOURCE_DIR ".\build\Snapmaker_Orca"
!endif
!define PACK_SOURCE_DIR "${SOURCE_DIR}"
; 64-bit app: use PROGRAMFILES64 so default path is C:\Program Files\Snapmaker_Orca, not (x86)
!define INSTALL_DIR_RUNTIME "$PROGRAMFILES64\Snapmaker_Orca"
InstallDir "${INSTALL_DIR_RUNTIME}"
!ifndef OUTPUT_FILE
!define OUTPUT_FILE "Snapmaker_Orca_Windows_Installer_V${VERSION}.exe"
!endif
; License page: show LICENSE.txt from repo root (same dir as this .nsi)
!ifndef LICENSE_FILE
!define LICENSE_FILE ".\LICENSE.txt"
!endif
RequestExecutionLevel admin
; No /SOLID to avoid "Internal compiler error #12345: error mmapping datablock"
SetCompressor lzma
VIProductVersion "${VERSION}.0"
VIAddVersionKey "ProductName" "${PRODUCT_NAME}"
VIAddVersionKey "Comments" "Snapmaker Orca is an open source slicer for FDM printers"
VIAddVersionKey "CompanyName" "${PRODUCT_PUBLISHER}"
VIAddVersionKey "LegalCopyright" "Copyright (C) ${PRODUCT_PUBLISHER}"
VIAddVersionKey "FileDescription" "${PRODUCT_NAME} ${VERSION} Installer"
VIAddVersionKey "FileVersion" "${VERSION}"
VIAddVersionKey "ProductVersion" "${VERSION}"
VIAddVersionKey "InternalName" "${PRODUCT_NAME}"
VIAddVersionKey "LegalTrademarks" ""
VIAddVersionKey "OriginalFilename" "${OUTPUT_FILE}"
; Installer and uninstaller icon: set by build_and_pack.bat via /DICON_FILE=path (e.g. Snapmaker_Orca.ico or snapmaker.ico)
!ifdef ICON_FILE
!define MUI_ICON "${ICON_FILE}"
!define MUI_UNICON "${ICON_FILE}"
!else
!define MUI_ICON ".\resources\images\Snapmaker_Orca.ico"
!define MUI_UNICON ".\resources\images\Snapmaker_Orca.ico"
!endif
!define MUI_WELCOMEPAGE_TITLE "Welcome to ${PRODUCT_NAME} Setup"
!define MUI_WELCOMEPAGE_TEXT "This wizard will guide you through the installation of ${PRODUCT_NAME} ${VERSION}.$\r$\n$\r$\nClick Next to continue."
!insertmacro MUI_PAGE_WELCOME
!ifdef LICENSE_FILE
!define MUI_LICENSEPAGE_CHECKBOX
!insertmacro MUI_PAGE_LICENSE "${LICENSE_FILE}"
!endif
!insertmacro MUI_PAGE_COMPONENTS
!define MUI_DIRECTORYPAGE_TEXT_TOP "Choose the folder in which to install ${PRODUCT_NAME}."
!insertmacro MUI_PAGE_DIRECTORY
!insertmacro MUI_PAGE_INSTFILES
!define MUI_FINISHPAGE_RUN
!define MUI_FINISHPAGE_RUN_TEXT "Run ${PRODUCT_NAME}"
!define MUI_FINISHPAGE_RUN_FUNCTION "LaunchApp"
!define MUI_FINISHPAGE_LINK "Visit ${PRODUCT_NAME} website"
!define MUI_FINISHPAGE_LINK_LOCATION "${PRODUCT_WEB_SITE}"
!insertmacro MUI_PAGE_FINISH
!insertmacro MUI_UNPAGE_CONFIRM
!insertmacro MUI_UNPAGE_INSTFILES
!insertmacro MUI_LANGUAGE "SimpChinese"
!insertmacro MUI_LANGUAGE "English"
Name "${PRODUCT_NAME} ${VERSION}"
OutFile "${OUTPUT_FILE}"
Section "Main program" SecMain
SectionIn RO
SetOutPath "$INSTDIR"
DetailPrint "Installing ${PRODUCT_NAME}..."
DetailPrint "Target dir: $INSTDIR"
DetailPrint "Copying files..."
; PACK_SOURCE_DIR = compile time only. At runtime this File extracts from embedded payload to $INSTDIR. Exclude include and lib dirs.
File /r /x "*.pdb" /x "*.ilk" /x "*.exp" /x "*.lib" /x "*.obj" /x "*.idb" /x "*.tlog" /x "*.h" /x "*.hpp" /x "*.c" /x "*.cpp" /x "*.cxx" /x "*.cc" /x "*.vcxproj" /x "*.vcxproj.filters" /x "*.sln" /x "*.cmake" /x "*.py" /x "*.md" /x "*.vcxproj.user" /x "CMakeFiles" /x "RelWithDebInfo" /x "Debug" /x "MinSizeRel" /x ".vs" /x "vcpkg_installed" /x "*.dir" /x "include\*" /x "lib\*" "${PACK_SOURCE_DIR}\*.*"
IfFileExists "$INSTDIR\snapmaker-orca.exe" 0 extract_error
DetailPrint "Creating uninstaller..."
WriteUninstaller "$INSTDIR\Uninstall.exe"
DetailPrint "Writing registry..."
WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "DisplayName" "${PRODUCT_NAME}"
WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "UninstallString" "$INSTDIR\Uninstall.exe"
WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "InstallLocation" "$INSTDIR"
WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "DisplayIcon" "$INSTDIR\snapmaker-orca.exe"
WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "Publisher" "${PRODUCT_PUBLISHER}"
WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "DisplayVersion" "${VERSION}"
WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "URLInfoAbout" "${PRODUCT_WEB_SITE}"
WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "HelpLink" "${PRODUCT_WEB_SITE}"
WriteRegDWORD ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "NoModify" 1
WriteRegDWORD ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "NoRepair" 1
WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_INSTALL_KEY}" "Version" "${VERSION}"
WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_INSTALL_KEY}" "InstallPath" "$INSTDIR"
DetailPrint "Installation complete!"
Goto end_section
extract_error:
MessageBox MB_OK|MB_ICONSTOP "Installation failed: snapmaker-orca.exe was not found in the package. The installer may be corrupted."
Abort
end_section:
SectionEnd
Section "Desktop shortcut" SecDesktop
DetailPrint "Creating desktop shortcut..."
CreateShortcut "$DESKTOP\Snapmaker Orca.lnk" "$INSTDIR\snapmaker-orca.exe" "" "$INSTDIR\snapmaker-orca.exe" 0
SectionEnd
Section "Start menu shortcut" SecStartMenu
DetailPrint "Creating start menu shortcut..."
CreateDirectory "$SMPROGRAMS\${PRODUCT_NAME}"
CreateShortcut "$SMPROGRAMS\${PRODUCT_NAME}\Snapmaker Orca.lnk" "$INSTDIR\snapmaker-orca.exe" "" "$INSTDIR\snapmaker-orca.exe" 0
CreateShortcut "$SMPROGRAMS\${PRODUCT_NAME}\Uninstall.lnk" "$INSTDIR\Uninstall.exe" "" "$INSTDIR\Uninstall.exe" 0
SectionEnd
!insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN
!insertmacro MUI_DESCRIPTION_TEXT ${SecMain} "Install ${PRODUCT_NAME} and all required files."
!insertmacro MUI_DESCRIPTION_TEXT ${SecDesktop} "Create a desktop shortcut for ${PRODUCT_NAME}."
!insertmacro MUI_DESCRIPTION_TEXT ${SecStartMenu} "Create a start menu shortcut for ${PRODUCT_NAME}."
!insertmacro MUI_FUNCTION_DESCRIPTION_END
Section "Uninstall"
DetailPrint "Uninstalling ${PRODUCT_NAME}..."
DetailPrint "Checking for running processes..."
nsExec::ExecToLog 'taskkill /F /IM snapmaker-orca.exe /T'
Sleep 500
DetailPrint "Removing desktop shortcut..."
Delete "$DESKTOP\Snapmaker Orca.lnk"
Delete "$DESKTOP\${PRODUCT_NAME}.lnk"
DetailPrint "Removing start menu shortcut..."
RMDir /r "$SMPROGRAMS\${PRODUCT_NAME}"
DetailPrint "Removing install directory..."
RMDir /r /REBOOTOK "$INSTDIR"
RMDir "$INSTDIR"
DetailPrint "Removing registry entries..."
DeleteRegKey ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}"
DeleteRegKey ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_INSTALL_KEY}"
DeleteRegKey HKCU "${PRODUCT_INSTALL_KEY}"
DetailPrint "Uninstall complete!"
SectionEnd
Function LaunchApp
ExecShell "open" "$INSTDIR\snapmaker-orca.exe"
FunctionEnd
Function .onInit
ReadRegStr $R0 ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "UninstallString"
StrCmp $R0 "" done
MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION \
"${PRODUCT_NAME} is already installed.$\n$\nClick OK to uninstall the old version, or Cancel to abort." \
IDOK uninst
Abort
uninst:
ClearErrors
ExecWait '$R0 _?=$INSTDIR'
IfErrors no_remove_uninstaller done
no_remove_uninstaller:
done:
FunctionEnd
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+51
View File
@@ -1975,6 +1975,7 @@ ansicolor
--------------------------------------------------------------------------------
args
csslib
logging
Copyright 2013, the Dart project authors.
@@ -5368,6 +5369,7 @@ SUCH DAMAGE.
cross_file
flutter_lints
flutter_plugin_android_lifecycle
go_router
multicast_dns
path_provider
path_provider_android
@@ -8504,6 +8506,31 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
--------------------------------------------------------------------------------
flutter_html
MIT License
Copyright (c) 2019-2022 The flutter_html developers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
--------------------------------------------------------------------------------
flutter_image_compress
flutter_image_compress_common
@@ -32846,6 +32873,30 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
list_counter
MIT License
Copyright (c) 2022 The list_counter developers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
--------------------------------------------------------------------------------
loading_animation_widget
@@ -302,7 +302,32 @@
"dialog_filament_type_not_match_tips": "This nozzle filament type does not match the preset filament type of the model. Please replace this nozzle filament type or change the preset filament type",
"dialog_filament_type_none_tips": "Filaments were not identified. Please mount the filaments on the device or edit the filament information",
"Features are under development, please stay tuned": "Features are under development, please stay tuned",
"Model": "Model",
"Coming soon...": "Coming soon...",
"Search models...": "Search models...",
"Library": "Library",
"Models": "Models",
"Model Detail": "Model Detail",
"Published": "Published",
"Collect": "Collect",
"Download and Open": "Download & Open",
"Direct print": "Direct print",
"direct_print_tips": "“Print Now” is only available in Cloud Mode",
"Case tags": "Case tags",
"Description": "Description",
"Copyright": "Copyright",
"model_detail_license_title": "Standard Digital File License",
"model_detail_license_description": "This license restricts the use, distribution and commercial use of the digital file or its 3D object. Please comply with the relevant terms.",
"Weight": "Weight",
"Time": "Time",
"Layers": "Layers",
"Height": "Height",
"Profiles": "Profiles",
"Image display": "Image display",
"No models found": "No models found",
"Try a different search term": "Try a different search term",
"Retry": "Retry",
"Print preprocessing": "Print preprocessing",
"delete_selected_files_confirm_title": "Confirm Delete Selected Files",
"delete_selected_files_confirm_message": "Are you sure you want to delete the selected files? This action cannot be undone.",
"clear_all_files_confirm_title": "Confirm Clear All Files",
@@ -331,7 +356,8 @@
"camera_auto_shutdown_message": "The camera has been unused for a long time. To reduce power consumption, it has automatically shut down.",
"got_it": "Got it",
"monitoring_module_hibernated": "Note : Monitoring Module hibernated. Click Play to wake it up.",
"Plates": "Plates",
"Split Plate": "Split Plate",
"Check": "Check",
"default_exception_title": "Exception",
"Error Code": "Error Code",
@@ -1146,5 +1172,24 @@
"error_0002053200000001_title": "Possible Foreign Object Detected on Print Bed",
"error_0002053200000001_desc": "Please check and remove any foreign objects. If none are found, click Continue to resume printing.",
"error_0002053200000002_title": "Possible Spaghetti Defect Detected",
"error_0002053200000002_desc": "Please inspect the model. If any defects are acceptable—or none are found—click Continue to resume printing."
"error_0002053200000002_desc": "Please inspect the model. If any defects are acceptable—or none are found—click Continue to resume printing.",
"Load More": "Load More",
"No More Data": "No More Data",
"Error Occurred": "Error Occurred",
"No Models Found": "No Models Found",
"Not Implemented Yet": "Not implemented yet, please try again later",
"Nozzle": "Nozzle",
"Model Library": "Model Library",
"Print Plates": "Print Plates",
"Copyright Notice": "Copyright Notice",
"Model Description": "Model Description",
"Recommendation for You": "Recommendation for You",
"units": "units",
"Total": "Total",
"Filament List": "Filament List",
"Collapse": "Collapse",
"Expand": "Expand",
"Untitled Config": "Untitled Config",
"Display Area": "Display Area",
"Print Now is only available in Cloud Mode": "Print Now is only available in Cloud Mode"
}
@@ -230,6 +230,7 @@
"Pull down refresh": "下拉刷新",
"Click to load more": "点击加载更多",
"User not logged in": "用户未登录",
"Not logged in account": "未登录账户",
@@ -293,9 +294,6 @@
"File information": "文件信息",
"Model information": "模型信息",
"Estimated material": "预估用料",
"Estimated time": "预估用时",
"File name": "文件名称",
"Select printer": "选择打印机",
"Edit filament": "编辑耗材",
@@ -307,7 +305,7 @@
"Time-lapse camera": "延时摄影",
"Bed leveling": "热床调平",
"Flow calibrate": "流量校准",
"Extrusion flow calibrate": "挤出流量校准",
"Extrusion Flow Calibration": "挤出流量校准",
"When enabled, the printer will automatically calibrate flow compensation before printing. Recommended after each filament change.": "启用后,打印机将在打印前自动校准流量补偿。每次更换丝材后建议执行。",
"Please select filament type": "请选择耗材类型",
@@ -555,6 +553,30 @@
"dialog_filament_type_none_tips": "未识别到耗材,请在设备上挂载耗材或编辑耗材信息",
"Features are under development, please stay tuned": "功能开发中, 敬请期待",
"File bytes is empty": "获取文件内容为空,请重新发起切片操作",
"Model": "模型",
"Coming soon...": "即将推出...",
"Search models...": "搜索模型...",
"Library": "素材库",
"Models": "模型",
"Model Detail": "模型详情",
"Published": "发布于",
"Collect": "收藏",
"Download and Open": "下载并打开",
"Direct print": "直接打印",
"direct_print_tips": "提示:直接打印仅支持云联模式",
"Case tags": "案例标签",
"Description": "描述",
"model_detail_license_title": "标准数字文件许可",
"model_detail_license_description": "该许可证限制了对数字文件或该对象的3D模型的使用、传播与商业用途,请遵守相关条款。该许可证限制了对数字文件或该对象的3D模型的使用、传播与商业用途,请遵守相关条款。",
"Weight": "重量",
"Time": "耗时",
"Layers": "层数",
"Height": "高度",
"Profiles": "打印配置",
"Image display": "图片展示",
"No models found": "未找到模型",
"Try a different search term": "请尝试其他搜索关键词",
"Print preprocessing": "打印预处理",
"File uploaded successfully": "文件上传成功",
"file_uploaded_success_format": "已上传完成,将自动跳转到设备控制页面 {} 秒",
"The device failed to upload the model file. Please check the device network and upload it again.": "设备上传模型文件失败,请检查设备网络并重新上传",
@@ -577,7 +599,7 @@
"this command requires the printer to be online, currently the printer is not online": "当前设备不在线",
"Bind device failed, please check if pin code is correct.": "绑定失败,可能是 PIN 码输入错误或者是 PIN 码检测失败",
"Bind device failed, please check if device locale setting is not same as client.": "绑定失败,软件与设备区域设置不一致",
"The device is in LAN mode and cannot download print files from the cloud. Please switch to Cloud Mode and try again.": "设备处于局域网模式,不支持从云端下载打印文件,请切换至云模式后重试。",
"delete_selected_files_confirm_title": "确认删除选中文件",
"delete_selected_files_confirm_message": "确定要删除选中的文件吗?此操作无法撤销。",
"clear_all_files_confirm_title": "确认清空所有文件",
@@ -603,6 +625,8 @@
"camera_auto_shutdown_message": "摄像头长时间未使用,为了节省电量,已自动关闭。",
"got_it": "知道了",
"monitoring_module_hibernated": "提示:监控模块已休眠,点击播放按钮即可唤醒",
"Plates": "盘",
"Split Plate": "分盘",
"Check": "查看",
"default_exception_title": "异常",
@@ -1418,5 +1442,23 @@
"error_0002053200000001_title": "检测到床面可能有异物",
"error_0002053200000001_desc": "请检查并清理异物后,点击继续以恢复打印;若检查无误,也可直接点击继续。",
"error_0002053200000002_title": "检测到可能存在炒面缺陷",
"error_0002053200000002_desc": "请检查模型,若缺陷可接受,点击继续以恢复打印;若检查无误,也可直接点击继续。"
"error_0002053200000002_desc": "请检查模型,若缺陷可接受,点击继续以恢复打印;若检查无误,也可直接点击继续。",
"Load More": "加载更多",
"No More Data": "没有更多数据",
"Error Occurred": "发现未知错误",
"No Models Found": "未找到模型",
"Not Implemented Yet": "还未实现,请稍后再试",
"Nozzle": "喷嘴",
"Model Library":"模型库",
"Print Plates": "打印盘",
"Copyright Notice": "版权说明",
"Model Description": "模型简介",
"Recommendation for You": "官方推荐",
"units": "个",
"Total": "总量",
"Filament List": "多色耗材清单",
"Collapse": "收起",
"Expand": "展开",
"Untitled Config": "未命名配置",
"Display Area": "展示区域"
}
@@ -11,6 +11,6 @@ _flutter.buildConfig = {"engineRevision":"b8800d88be4866db1b15f8b954ab2573bba996
_flutter.loader.load({
serviceWorkerSettings: {
serviceWorkerVersion: "1507942654"
serviceWorkerVersion: "977786741"
}
});
@@ -3,33 +3,42 @@ const MANIFEST = 'flutter-app-manifest';
const TEMP = 'flutter-temp-cache';
const CACHE_NAME = 'flutter-app-cache';
const RESOURCES = {"flutter_bootstrap.js": "0a7c8042e86cd3cb2569ca1fc998cfd7",
"version.json": "31cecd6c4768d61e6cfee5eb2d82cfa5",
"index.html": "626847718ec5c240e19799fa3d4aa135",
"/": "626847718ec5c240e19799fa3d4aa135",
"main.dart.js": "e3000b9495811b970bca43c9b55ce1d3",
const RESOURCES = {"flutter_bootstrap.js": "89d1216e3492ae8feb57f21e79a5300b",
"version.json": "b297d1d7d8c6a08be1e30d950702767c",
"index.html": "2753effad264bd7ecad1460aa8a6e57f",
"/": "2753effad264bd7ecad1460aa8a6e57f",
"main.dart.js": "76ac3516e1e3b1d9a02bc5cfd8faee8b",
"flutter.js": "f31737fb005cd3a3c6bd9355efd33061",
"version.changelog": "11ce0fece0721d2530ca068d494b62e2",
"version.changelog": "68e42820da08df9add65e04dc17983e2",
"favicon.png": "be8d1ab28c20907c9869c345d0482962",
"icons/Icon-192.png": "ab1f25ced1559729e334de938eae91a5",
"icons/Icon-maskable-192.png": "e41e8489c0f6a822acf8dab362e112b7",
"icons/Icon-maskable-512.png": "4870fb6720f4fcad016cb582589d136d",
"icons/Icon-512.png": "343022ac1c56796cb7ff635faf0646ef",
"manifest.json": "901d86fb8842ec0d66225a542131d689",
"assets/AssetManifest.json": "20cbacdb1780e93d6fb36d8f0e179822",
"assets/NOTICES": "be80e66ea53f8a268fb7677969331e98",
"assets/AssetManifest.json": "0ca1adeaeab52709b07b5d10f289d27e",
"assets/NOTICES": "34b07d2be0ec8d604f4e7b59c728c1d7",
"assets/FontManifest.json": "0dc3d44d47c5e2636cdca4babafb2396",
"assets/AssetManifest.bin.json": "dc681d818e8a6e5fc76e929915894fa3",
"assets/AssetManifest.bin.json": "65479ef4a322f558edbe98c4ccb24ccf",
"assets/packages/lava_model_station/assets/404.png": "e8a45c994c2f6f551cf1e052f64dba1c",
"assets/packages/lava_model_station/assets/placehoder-image.png": "fd0b3547e2b90c124112401d7a8f6a02",
"assets/packages/lava_model_station/assets/svgs/iconPlate.svg": "05acab23d908534f0660b4f06cc36e8d",
"assets/packages/lava_model_station/assets/svgs/iconTime.svg": "bcddee2512587e441a39fbdf4b0c1d70",
"assets/packages/lava_model_station/assets/svgs/iconNozzle.svg": "fa46a390b0c9db063e289af55b55329e",
"assets/packages/lava_model_station/assets/svgs/iconWeight.svg": "bfe3246df9e15f08bc5208d34ae3a814",
"assets/packages/lava_model_station/assets/empty-box.png": "f0fcb1ead826eec9ff565c2ac0dfd1da",
"assets/packages/lava_device_control/assets/files/filament.json": "e78d490824eb52bf5fd3adcb07296cb0",
"assets/packages/cupertino_icons/assets/CupertinoIcons.ttf": "391ff5f9f24097f4f6e4406690a06243",
"assets/packages/fluttertoast/assets/toastify.js": "56e2c9cedd97f10e7e5f1cebd85d53e3",
"assets/packages/fluttertoast/assets/toastify.css": "a85675050054f179444bc5ad70ffc635",
"assets/packages/wakelock_plus/assets/no_sleep.js": "7748a45cd593f33280669b29c2c8919a",
"assets/shaders/ink_sparkle.frag": "ecc85a2e95f5e9f53123dcaf8cb9b6ce",
"assets/AssetManifest.bin": "26378ad7581792ebae1a2fa9dfd66b24",
"assets/fonts/MaterialIcons-Regular.otf": "694c59090b9196ac07e9b4e8368882f8",
"assets/assets/i10n/zh-CN.json": "131f0702824659846d2b184440c5003a",
"assets/assets/i10n/en.json": "a03ae4b5f57a2978e69066b032f4aded",
"assets/AssetManifest.bin": "e4c6ca6d50f07cdddb11a96649efa238",
"assets/fonts/MaterialIcons-Regular.otf": "165e0312a2248a5fe696bc445a5c4e9a",
"assets/assets/mock_data/model_detail.json": "9c3ce3e4bb3dbe78b74fc2d11d152485",
"assets/assets/mock_data/model_station_list.json": "1dcbd846d8b97bae543caba6ce135040",
"assets/assets/i10n/zh-CN.json": "e7d2b9a14187a607ea756958669d4754",
"assets/assets/i10n/en.json": "b1183acc5faf8100de758a6cc876de10",
"assets/assets/images/deviceNoResponse.webp": "1ca23a7feedfdc34362ea5789ccf895b",
"assets/assets/images/deviceAuthorized.webp": "8eb814193bed15cec22658018871aba8",
"assets/assets/images/IpInputGuide.webp": "06c11ce1dadc2910676aec6d40a5eea5",
@@ -52,6 +61,7 @@ const RESOURCES = {"flutter_bootstrap.js": "0a7c8042e86cd3cb2569ca1fc998cfd7",
"assets/assets/images/deviceInvalidVersion.webp": "66e3b61ac908b900761bf014e92c1d3d",
"assets/assets/svgs/iconMainCooling.svg": "55b38461348e477abac33fdda8f98e32",
"assets/assets/svgs/iconCloseWhite.svg": "21e00e7b7a7031241d82983eef24b416",
"assets/assets/svgs/modelStation.svg": "dfd8ccb848b39df409d943c048c36a84",
"assets/assets/svgs/iconClose.svg": "f6db4c0e4369cc05ae28d3bea8d5b1ad",
"assets/assets/svgs/loginPlatformApple.svg": "be43d78435feca50bbabad292a1039c7",
"assets/assets/svgs/iconNotific.svg": "27082276596d830c36e1f5d0902b3929",
File diff suppressed because one or more lines are too long
+6 -17
View File
@@ -1,20 +1,9 @@
# Changelog
# 更新日志 / Changelog
## 2026-03-06
## [版本号] 2026年1月30日
### Features
- Feature: Added a new model entry
### 修复 / Fixed
- **预打印页面设备耗材信息获取失败**
Fixed device consumable information retrieval failure on pre-print page
- 问题描述:在预打印页面无法正确获取到设备的耗材状态信息
- Issue: Device consumable status could not be retrieved correctly on the pre-print page
- **附近设备连接功能异常**
Fixed nearby device connection failure
- 问题描述:通过"附近设备"功能搜索到的设备无法正常连接
- Issue: Devices discovered via "Nearby Devices" feature could not be connected
- **局域网设备文件传输失败**
Fixed file transfer failure for LAN-only devices
- 问题描述:仅支持局域网连接的设备通过IP地址连接后,无法发起文件传输
- Issue: File transfer could not be initiated for LAN-only devices after connecting via IP address
### Bugfix
Fixed the issue where devices would automatically disconnect when connected over Lan network
+1 -1
View File
@@ -1 +1 @@
{"app_name":"orca","version":"2.2.4","build_number":"20260205155654","package_name":"orca"}
{"app_name":"orca","version":"2.2.8","build_number":"20260306205559","package_name":"orca"}
+466
View File
@@ -0,0 +1,466 @@
#!/bin/bash
# macOS 应用签名、打包、公证完整流程脚本
# 用法: ./scripts/sign_and_package.sh [arm64|x86_64] [app_path]
set -e
# 检测架构参数
ARCH="${1:-$(uname -m)}"
# 标准化架构名称
case "$ARCH" in
arm64|aarch64)
ARCH="arm64"
;;
x86_64|x86-64|amd64)
ARCH="x86_64"
;;
*)
echo "错误: 不支持的架构 $ARCH"
echo "用法: $0 [arm64|x86_64] [app_path]"
exit 1
;;
esac
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
BUILD_DIR="$PROJECT_DIR/build/$ARCH"
APP_NAME="Snapmaker_Orca"
APP_NAME_EX="Snapmaker Orca"
DMG_NAME="Snapmaker_Orca_${ARCH}.dmg"
# 证书配置
CERTIFICATE_ID="Developer ID Application: Shenzhen Snapmaker Technologies Co., Ltd. (5NGD3B3V37)"
ENTITLEMENTS="$PROJECT_DIR/scripts/disable_validation.entitlements"
# ============================================
# 公证凭据配置(已设置)
# ============================================
NOTARY_APPLE_ID="snapmaker-app@snapmaker.com"
NOTARY_TEAM_ID="5NGD3B3V37"
#NOTARY_KEYCHAIN_PROFILE="snapmaker"
NOTARY_PASSWORD="guhi-nuxy-mgnh-cbqs"
echo "=========================================="
echo "macOS 应用签名、打包、公证完整流程"
echo "=========================================="
echo "架构: $ARCH"
echo "证书: $CERTIFICATE_ID"
echo "TEAM_ID: 5NGD3B3V37"
echo "项目目录: $PROJECT_DIR"
echo
# ============================================
# 查找应用
# ============================================
# 如果提供了 app 路径
if [ -n "$2" ]; then
SOURCE_APP="$2"
if [ ! -d "$SOURCE_APP" ]; then
echo "错误: 找不到应用: $SOURCE_APP"
exit 1
fi
echo "使用指定应用: $SOURCE_APP"
else
# 自动查找编译好的 app
for possible_path in \
"$BUILD_DIR/src/Release/$APP_NAME.app" \
"$BUILD_DIR/src/RelWithDebInfo/$APP_NAME.app" \
"$BUILD_DIR/src/$APP_NAME.app" \
"$BUILD_DIR/src/Debug/$APP_NAME.app"
do
if [ -d "$possible_path" ]; then
SOURCE_APP="$possible_path"
echo "找到应用: $SOURCE_APP"
break
fi
done
# 检查空格版本名称
if [ -z "$SOURCE_APP" ]; then
for possible_path in \
"$BUILD_DIR/src/Release/Snapmaker Orca.app" \
"$BUILD_DIR/Snapmaker_Orca/Snapmaker Orca.app"
do
if [ -d "$possible_path" ]; then
SOURCE_APP="$possible_path"
echo "找到应用: $SOURCE_APP"
break
fi
done
fi
if [ -z "$SOURCE_APP" ]; then
echo "错误: 在 $BUILD_DIR 中找不到编译好的 $APP_NAME.app"
echo "请先编译 $ARCH 版本: ./build_release_macos.sh -s -a $ARCH"
exit 1
fi
fi
# 创建临时工作目录
WORK_DIR="$BUILD_DIR/sign_package"
STAGING_DIR="$WORK_DIR/staging"
rm -rf "$WORK_DIR"
mkdir -p "$STAGING_DIR"
# 清理所有可能的残留挂载点(在开始工作前)
echo "清理可能的残留挂载点..."
for mount_point in /Volumes/Snapmaker* /Volumes/Snapmaker*; do
if [ -d "$mount_point" ]; then
echo " 卸载: $mount_point"
hdiutil detach "$mount_point" -force 2>/dev/null || true
fi
done
sleep 1
# 复制应用到工作目录
echo
echo "=========================================="
echo "步骤 1/6: 复制应用"
echo "=========================================="
echo "复制应用到工作目录..."
cp -R "$SOURCE_APP" "$STAGING_DIR/$APP_NAME.app"
FINAL_APP="$STAGING_DIR/$APP_NAME.app"
# 删除 .DS_Store 文件
find "$FINAL_APP" -name '.DS_Store' -delete
# 删除 PkgInfo 文件(冗余文件)
rm -f "$FINAL_APP/Contents/PkgInfo" 2>/dev/null || true
# 清理所有扩展属性(包括 com.apple.quarantine),避免 Gatekeeper 问题
echo "清理扩展属性..."
xattr -cr "$FINAL_APP"
# ============================================
# 打包外部依赖库
# ============================================
APP_MACOS_DIR="$FINAL_APP/Contents/MacOS"
APP_FRAMEWORKS_DIR="$FINAL_APP/Contents/Frameworks"
EXECUTABLE="$APP_MACOS_DIR/$APP_NAME"
# 确保 Frameworks 目录存在
mkdir -p "$APP_FRAMEWORKS_DIR"
echo
echo "检查并打包外部依赖库..."
# 查找所有外部依赖(非系统库)
EXTERNAL_LIBS=$(otool -L "$EXECUTABLE" | grep -E "opt/homebrew|usr/local|opt/local" | awk '{print $1}')
if [ -n "$EXTERNAL_LIBS" ]; then
echo "发现外部依赖:"
echo "$EXTERNAL_LIBS"
echo
for LIB_PATH in $EXTERNAL_LIBS; do
if [ -f "$LIB_PATH" ]; then
LIB_NAME=$(basename "$LIB_PATH")
echo "处理: $LIB_NAME"
# 获取实际的库文件路径(处理符号链接)- macOS 兼容方式
if command -v realpath &> /dev/null; then
REAL_LIB=$(realpath "$LIB_PATH" 2>/dev/null || echo "$LIB_PATH")
else
# macOS 不支持 realpath/readlink -f,使用 perl
REAL_LIB=$(perl -MCwd=abs_path -e 'print abs_path(shift)' "$LIB_PATH" 2>/dev/null || echo "$LIB_PATH")
fi
REAL_NAME=$(basename "$REAL_LIB")
# 复制实际的库文件
if [ ! -f "$APP_FRAMEWORKS_DIR/$REAL_NAME" ]; then
cp "$REAL_LIB" "$APP_FRAMEWORKS_DIR/$REAL_NAME"
# 修改库的 ID 为文件名(不带路径)
install_name_tool -id "$REAL_NAME" "$APP_FRAMEWORKS_DIR/$REAL_NAME"
# 删除库中的 rpath(避免问题)
install_name_tool -delete_rpath "@loader_path/../lib" "$APP_FRAMEWORKS_DIR/$REAL_NAME" 2>/dev/null || true
install_name_tool -delete_rpath "@loader_path/lib" "$APP_FRAMEWORKS_DIR/$REAL_NAME" 2>/dev/null || true
fi
# 注释掉:不创建中间符号链接,避免冗余
# if [ "$LIB_NAME" != "$REAL_NAME" ]; then
# (cd "$APP_FRAMEWORKS_DIR" && ln -sf "$REAL_NAME" "$LIB_NAME")
# fi
# 更新可执行文件中的依赖引用
install_name_tool -change "$LIB_PATH" "@executable_path/../Frameworks/$REAL_NAME" "$EXECUTABLE" 2>/dev/null || true
install_name_tool -change "$REAL_LIB" "@executable_path/../Frameworks/$REAL_NAME" "$EXECUTABLE" 2>/dev/null || true
fi
done
echo
echo "已打包的依赖库:"
ls -la "$APP_FRAMEWORKS_DIR/"
else
echo "没有外部依赖需要处理"
fi
# 移除不需要的 rpath
echo
echo "清理 rpath..."
install_name_tool -delete_rpath "/opt/homebrew/lib" "$EXECUTABLE" 2>/dev/null || true
install_name_tool -delete_rpath "/usr/local/lib" "$EXECUTABLE" 2>/dev/null || true
install_name_tool -delete_rpath "/opt/local/lib" "$EXECUTABLE" 2>/dev/null || true
# 修复 Resources 符号链接 (如果是符号链接)
RESOURCES_LINK="$FINAL_APP/Contents/Resources"
if [ -L "$RESOURCES_LINK" ]; then
echo "修复 Resources 符号链接..."
RESOURCES_TARGET=$(readlink "$RESOURCES_LINK")
rm "$RESOURCES_LINK"
cp -R "$RESOURCES_TARGET" "$RESOURCES_LINK"
fi
# 验证依赖
echo
echo "验证最终依赖:"
otool -L "$EXECUTABLE" | grep -E "@executable|libzstd|libsentry" || echo "无特殊依赖"
# ============================================
# 步骤 2/6: 签名应用
# ============================================
echo
echo "=========================================="
echo "步骤 2/6: 签名应用"
echo "=========================================="
APP_FRAMEWORKS_DIR="$FINAL_APP/Contents/Frameworks"
APP_MACOS_DIR="$FINAL_APP/Contents/MacOS"
EXECUTABLE="$APP_MACOS_DIR/$APP_NAME"
# 2.1 移除现有签名
echo "2.1 移除现有签名..."
codesign --remove-signature "$FINAL_APP" 2>/dev/null || true
# 2.2 签名 Frameworks 和动态库(使用 runtime 选项)
echo "2.2 签名 Frameworks 和动态库(使用 runtime 选项)..."
if [ -d "$APP_FRAMEWORKS_DIR" ]; then
# 签名所有 .framework
for framework in "$APP_FRAMEWORKS_DIR"/*.framework; do
if [ -d "$framework" ]; then
echo " - 签名: $(basename "$framework")"
codesign --force --verbose --options runtime --timestamp --sign "$CERTIFICATE_ID" "$framework" 2>/dev/null || true
fi
done
# 签名所有 .dylib
for dylib in "$APP_FRAMEWORKS_DIR"/*.dylib; do
if [ -f "$dylib" ]; then
echo " - 签名: $(basename "$dylib")"
codesign --force --verbose --options runtime --timestamp --sign "$CERTIFICATE_ID" "$dylib"
fi
done
# 签名其他可能存在的库文件(如 .so)
for lib in "$APP_FRAMEWORKS_DIR"/*.*; do
if [ -f "$lib" ]; then
case "$lib" in
*.dylib) ;; # 已处理,跳过
*)
echo " - 签名: $(basename "$lib")"
codesign --force --verbose --options runtime --timestamp --sign "$CERTIFICATE_ID" "$lib"
;;
esac
fi
done
fi
# 2.3 签名辅助工具
echo "2.3 签名辅助工具(使用 runtime 选项)..."
if [ -f "$APP_MACOS_DIR/crashpad_handler" ]; then
echo " - 签名: crashpad_handler"
codesign --force --verbose --options runtime --timestamp --sign "$CERTIFICATE_ID" "$APP_MACOS_DIR/crashpad_handler"
fi
# 2.4 签名整个 app bundle(应用 entitlements
echo "2.4 签名整个 app bundle(应用 entitlements..."
echo " 这会签名所有组件并将 entitlements 应用到主可执行文件"
codesign --force --verbose --options runtime --timestamp \
--entitlements "$ENTITLEMENTS" \
--sign "$CERTIFICATE_ID" \
"$FINAL_APP"
# 2.5 验证签名和 entitlements
echo "2.5 验证签名和 entitlements..."
echo " 检查签名..."
codesign -vvv "$FINAL_APP" 2>&1 | grep -E "valid on disk|Authority|TeamIdentifier" | head -5
echo ""
echo " 检查 entitlements..."
if codesign -d --entitlements - "$FINAL_APP" 2>&1 | grep -q "com.apple.security.cs.disable-library-validation"; then
echo " ✓ Entitlements 正确嵌入!"
else
echo "警告: 预期的 entitlements 未找到"
fi
# ============================================
# 步骤 3/6: 创建并签名 DMG
# 流程与 GitHub Actions 完全一致:准备内容 -> 一步 create UDZO(不挂载)-> 签名 DMG
# 不挂载可避免本地「操作不被允许」;打开 DMG 后为系统默认图标布局
# ============================================
echo
echo "=========================================="
echo "步骤 3/6: 创建并签名 DMG"
echo "=========================================="
DMG_CONTENT_DIR="$WORK_DIR/dmg_content"
rm -rf "$DMG_CONTENT_DIR"
mkdir -p "$DMG_CONTENT_DIR"
rm -rf "$DMG_CONTENT_DIR/.fseventsd" 2>/dev/null || true
# 复制应用(显示名 Snapmaker Orca.app)并创建 Applications 符号链接(与 CI 一致)
echo "准备 DMG 内容..."
cp -R "$FINAL_APP" "$DMG_CONTENT_DIR/$APP_NAME_EX.app"
# 清理 DMG 内容中的扩展属性(重要!避免 Gatekeeper 问题)
xattr -cr "$DMG_CONTENT_DIR/$APP_NAME_EX.app"
ln -sfn /Applications "$DMG_CONTENT_DIR/Applications"
# 卷名不使用下划线,避免 macOS 安全机制阻止
DMG_VOLNAME="Snapmaker_Orca"
FINAL_DMG_PATH="$BUILD_DIR/$DMG_NAME"
rm -f "$FINAL_DMG_PATH"
# 再次清理可能残留的挂载点
if [ -d "/Volumes/$DMG_VOLNAME" ]; then
echo "检测到残留挂载点 /Volumes/$DMG_VOLNAME,正在强制卸载..."
hdiutil detach "/Volumes/$DMG_VOLNAME" -force 2>/dev/null || true
sleep 2
fi
# 检查是否有同名 DMG 已挂载
MOUNTED_DMG=$(hdiutil info | grep "/Volumes/$DMG_VOLNAME" || true)
if [ -n "$MOUNTED_DMG" ]; then
echo "警告: 发现已挂载的同名卷,尝试卸载..."
hdiutil info | grep "/Volumes/$DMG_VOLNAME" | grep -o '/dev/disk[0-9]*' | while read -r disk; do
hdiutil detach "$disk" -force 2>/dev/null || true
done
sleep 2
fi
echo "创建 DMG: $FINAL_DMG_PATH (卷名: $DMG_VOLNAME)"
if ! hdiutil create \
-volname "$DMG_VOLNAME" \
-srcfolder "$DMG_CONTENT_DIR" \
-ov \
-format UDZO \
-imagekey zlib-level=9 \
-o "$FINAL_DMG_PATH"; then
echo ""
echo "错误: hdiutil create 失败"
echo "尝试使用替代方法创建 DMG..."
# 备用方案:使用 mktemp 创建临时卷名
TEMP_VOLNAME="Snapmaker_Orca_$$"
if hdiutil create \
-volname "$TEMP_VOLNAME" \
-srcfolder "$DMG_CONTENT_DIR" \
-ov \
-format UDZO \
-imagekey zlib-level=9 \
-o "$FINAL_DMG_PATH"; then
echo "使用临时卷名创建成功"
else
echo "错误: DMG 创建失败,请手动检查 /Volumes 目录"
echo "运行 'ls -la /Volumes/' 查看挂载点"
echo "运行 'hdiutil info' 查看所有挂载的磁盘镜像"
exit 1
fi
fi
[ ! -f "$FINAL_DMG_PATH" ] && echo "错误: 未生成 DMG" && exit 1
# 签名 DMG
echo "签名 DMG..."
codesign --force --timestamp --sign "$CERTIFICATE_ID" "$FINAL_DMG_PATH"
echo "验证 DMG 签名..."
codesign -vvv "$FINAL_DMG_PATH" 2>&1 | head -3
rm -rf "$DMG_CONTENT_DIR"
echo ""
echo "=========================================="
echo "DMG 创建和签名完成!"
echo "=========================================="
echo "DMG: $FINAL_DMG_PATH"
echo "大小: $(du -h "$FINAL_DMG_PATH" | cut -f1)"
# ============================================
# 步骤 4/6: 公证 DMG
# ============================================
echo ""
echo "=========================================="
echo "步骤 4/6: 公证 DMG"
echo "=========================================="
# 判断是否可公证:检查密码是否已设置
echo "检查公证凭据..."
echo " Apple ID: $NOTARY_APPLE_ID"
echo " Team ID: $NOTARY_TEAM_ID"
if [ -z "$NOTARY_PASSWORD" ] || [ "$NOTARY_PASSWORD" = "__PLEASE_ENTER_PASSWORD__" ]; then
echo ""
echo "密码未设置!"
echo ""
echo "请在脚本中设置密码:"
echo " NOTARY_PASSWORD=\"your-app-specific-password\""
echo ""
echo "或者通过环境变量设置:"
echo " export NOTARY_PASSWORD=\"your-app-specific-password\""
echo ""
echo "跳过公证步骤..."
else
echo "✓ 密码已配置"
echo ""
echo "=========================================="
echo "步骤 5/6: 提交公证"
echo "=========================================="
echo "提交 DMG 到 Apple 公证服务..."
xcrun notarytool submit "$FINAL_DMG_PATH" \
--apple-id "$NOTARY_APPLE_ID" \
--team-id "$NOTARY_TEAM_ID" \
--password "$NOTARY_PASSWORD" \
--wait \
--progress
echo ""
echo "=========================================="
echo "步骤 6/6: 装订公证票据"
echo "=========================================="
echo "装订公证票据到 DMG..."
xcrun stapler staple "$FINAL_DMG_PATH"
# 验证公证结果
echo ""
echo "验证公证结果..."
xcrun stapler validate -v "$FINAL_DMG_PATH"
echo ""
echo "=========================================="
echo "公证完成!"
echo "=========================================="
echo "此 DMG 已签名并公证,可以在任何 Mac 上无缝运行"
fi
echo ""
echo "=========================================="
echo "完成!"
echo "=========================================="
echo "架构: $ARCH"
echo "应用: $FINAL_APP"
echo "DMG: $FINAL_DMG_PATH"
echo "证书: $CERTIFICATE_ID"
echo "TEAM_ID: 5NGD3B3V37"
echo ""
echo "使用方法:"
echo " 1. 打开 DMG: open $FINAL_DMG_PATH"
echo " 2. 将 $APP_NAME_EX.app 拖拽到 Applications 文件夹"
echo " 3. 从 Applications 运行应用"
echo "=========================================="
+38
View File
@@ -71,6 +71,44 @@ namespace common
#endif // _WIN32
return machineId;
}
std::string get_profile_version()
{
std::string versionFilePath = "";
#ifdef _WIN32
PWSTR pszPath = nullptr;
char* path = new char[MAX_PATH]();
size_t pathLength = 0;
HRESULT hr = SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, nullptr, &pszPath);
if (SUCCEEDED(hr)) {
wcstombs_s(&pathLength, path, MAX_PATH, pszPath, MAX_PATH);
CoTaskMemFree(pszPath);
}
std::string filePath = path;
versionFilePath = filePath + "\\" + std::string("Snapmaker_Orca\\system\\Snapmaker.json");
delete[] path;
#elif __APPLE__
const char* home_env = getenv("HOME");
versionFilePath = home_env;
versionFilePath = versionFilePath + "/Library/Application Support/Snapmaker_Orca/system/Snapmaker.json";
#else
#endif
std::ifstream json_file(versionFilePath);
if (!json_file.is_open()) {
std::ifstream json_file(versionFilePath);
return "";
}
nlohmann::json json_data;
json_file >> json_data;
std::string str_version = json_data.value("version", "");
return str_version;
}
std::string get_flutter_version()
{
+2
View File
@@ -23,6 +23,8 @@ namespace common
std::string get_flutter_version();
std::string get_profile_version();
std::string getMachineId();
std::string getLocalArea();
+63 -2
View File
@@ -10,6 +10,7 @@
#include <iostream>
#include <stdexcept>
#include <string>
#include <sstream>
#include <vector>
#include "libslic3r.h"
#include "clonable_ptr.hpp"
@@ -39,6 +40,12 @@ namespace Slic3r {
inline bool operator==(const FloatOrPercent& l, const FloatOrPercent& r) throw() { return l.value == r.value && l.percent == r.percent; }
inline bool operator!=(const FloatOrPercent& l, const FloatOrPercent& r) throw() { return !(l == r); }
inline bool operator< (const FloatOrPercent& l, const FloatOrPercent& r) throw() { return l.value < r.value || (l.value == r.value && int(l.percent) < int(r.percent)); }
inline std::ostream& operator<<(std::ostream& os, const FloatOrPercent& v) {
os << v.value;
if (v.percent)
os << "%";
return os;
}
}
namespace std {
@@ -344,6 +351,9 @@ public:
// Set a single vector item from either a scalar option or the first value of a vector option.vector of ConfigOptions.
// This function is useful to split values from multiple extrder / filament settings into separate configurations.
virtual void set_at(const ConfigOption *rhs, size_t i, size_t j) = 0;
// SM Orca: Copy a single element from source vector at src_idx to this vector at dst_idx
// This function is useful for applying physical extruder mapping to filament parameters
virtual void set_at(const ConfigOptionVectorBase* source, size_t dst_idx, size_t src_idx) = 0;
// Resize the vector of values, copy the newly added values from opt_default if provided.
virtual void resize(size_t n, const ConfigOption *opt_default = nullptr) = 0;
// Clear the values vector.
@@ -419,18 +429,69 @@ public:
T v = this->values.front();
this->values.resize(i + 1, v);
}
if (rhs->type() == this->type()) {
// Assign the first value of the rhs vector.
auto other = static_cast<const ConfigOptionVector<T>*>(rhs);
if (other->values.empty())
throw ConfigurationError("ConfigOptionVector::set_at(): Assigning from an empty vector");
// Log before assignment
std::stringstream before_ss;
before_ss << "[";
for (size_t k = 0; k < this->values.size(); ++k) {
if (k > 0) before_ss << ", ";
before_ss << this->values[k];
}
before_ss << "]";
// Log other vector
std::stringstream other_ss;
other_ss << "[";
for (size_t k = 0; k < other->values.size(); ++k) {
if (k > 0) other_ss << ", ";
other_ss << other->values[k];
}
other_ss << "]";
this->values[i] = other->get_at(j);
} else if (rhs->type() == this->scalar_type())
// Log after assignment
std::stringstream after_ss;
after_ss << "[";
for (size_t k = 0; k < this->values.size(); ++k) {
if (k > 0) after_ss << ", ";
after_ss << this->values[k];
}
after_ss << "]";
} else if (rhs->type() == this->scalar_type()) {
this->values[i] = static_cast<const ConfigOptionSingle<T>*>(rhs)->value;
else
} else
throw ConfigurationError("ConfigOptionVector::set_at(): Assigning an incompatible type");
}
// SM Orca: Copy a single element from source vector at src_idx to this vector at dst_idx
// Used for applying physical extruder mapping to filament parameters
void set_at(const ConfigOptionVectorBase* source, size_t dst_idx, size_t src_idx) override
{
auto* src_typed = dynamic_cast<const ConfigOptionVector<T>*>(source);
if (!src_typed || src_idx >= src_typed->size() || dst_idx >= this->size())
return;
// Handle nullable vectors - only copy if source value is not nil
if (this->nullable() && src_typed->nullable()) {
if (!src_typed->is_nil(src_idx)) {
this->values[dst_idx] = src_typed->values[src_idx];
}
} else if (!src_typed->nullable()) {
// Source is not nullable, always copy
this->values[dst_idx] = src_typed->values[src_idx];
}
// If source is nullable and value is nil, don't copy (keep existing value)
}
const T& get_at(size_t i) const
{
assert(! this->values.empty());
+12 -10
View File
@@ -6,8 +6,9 @@ namespace Slic3r {
double Extruder::m_share_E = 0.;
double Extruder::m_share_retracted = 0.;
Extruder::Extruder(unsigned int id, GCodeConfig *config, bool share_extruder) :
Extruder::Extruder(unsigned int id, unsigned int physical_extruder_id, GCodeConfig *config, bool share_extruder) :
m_id(id),
m_physical_extruder_id(physical_extruder_id),
m_config(config),
m_share_extruder(share_extruder)
{
@@ -157,24 +158,25 @@ double Extruder::filament_flow_ratio() const
}
// Return a "retract_before_wipe" percentage as a factor clamped to <0, 1>
// SM Orca: 回抽相关参数是挤出机属性,使用 m_physical_extruder_id
double Extruder::retract_before_wipe() const
{
return std::min(1., std::max(0., m_config->retract_before_wipe.get_at(m_id) * 0.01));
return std::min(1., std::max(0., m_config->retract_before_wipe.get_at(m_physical_extruder_id) * 0.01));
}
double Extruder::retraction_length() const
{
return m_config->retraction_length.get_at(m_id);
return m_config->retraction_length.get_at(m_physical_extruder_id);
}
double Extruder::retract_lift() const
{
return m_config->z_hop.get_at(m_id);
return m_config->z_hop.get_at(m_physical_extruder_id);
}
int Extruder::retract_speed() const
{
return int(floor(m_config->retraction_speed.get_at(m_id)+0.5));
return int(floor(m_config->retraction_speed.get_at(m_physical_extruder_id)+0.5));
}
bool Extruder::use_firmware_retraction() const
@@ -184,28 +186,28 @@ bool Extruder::use_firmware_retraction() const
int Extruder::deretract_speed() const
{
int speed = int(floor(m_config->deretraction_speed.get_at(m_id)+0.5));
int speed = int(floor(m_config->deretraction_speed.get_at(m_physical_extruder_id)+0.5));
return (speed > 0) ? speed : this->retract_speed();
}
double Extruder::retract_restart_extra() const
{
return m_config->retract_restart_extra.get_at(m_id);
return m_config->retract_restart_extra.get_at(m_physical_extruder_id);
}
double Extruder::retract_length_toolchange() const
{
return m_config->retract_length_toolchange.get_at(m_id);
return m_config->retract_length_toolchange.get_at(m_physical_extruder_id);
}
double Extruder::retract_restart_extra_toolchange() const
{
return m_config->retract_restart_extra_toolchange.get_at(m_id);
return m_config->retract_restart_extra_toolchange.get_at(m_physical_extruder_id);
}
double Extruder::travel_slope() const
{
return m_config->travel_slope.get_at(m_id) * PI / 180;
return m_config->travel_slope.get_at(m_physical_extruder_id) * PI / 180;
}
}
+8 -3
View File
@@ -11,7 +11,8 @@ class GCodeConfig;
class Extruder
{
public:
Extruder(unsigned int id, GCodeConfig *config, bool share_extruder);
// SM Orca: 添加 physical_extruder_id 参数用于支持耗材-挤出机映射
Extruder(unsigned int id, unsigned int physical_extruder_id, GCodeConfig *config, bool share_extruder);
virtual ~Extruder() {}
void reset() {
@@ -28,6 +29,8 @@ public:
}
unsigned int id() const { return m_id; }
// SM Orca: 获取物理挤出机ID
unsigned int physical_extruder_id() const { return m_physical_extruder_id; }
double extrude(double dE);
double retract(double length, double restart_extra);
@@ -75,12 +78,14 @@ public:
private:
// Private constructor to create a key for a search in std::set.
Extruder(unsigned int id) : m_id(id) {}
Extruder(unsigned int id) : m_id(id), m_physical_extruder_id(id) {}
// Reference to GCodeWriter instance owned by GCodeWriter.
GCodeConfig *m_config;
// Print-wide global ID of this extruder.
// Print-wide global ID of this extruder (filament index).
unsigned int m_id;
// SM Orca: 物理挤出机ID,用于查询挤出机属性(温度、回抽等)
unsigned int m_physical_extruder_id;
// Current state of the extruder axis, may be resetted if use_relative_e_distances.
double m_E;
// Current state of the extruder tachometer, used to output the extruded_volume() and used_filament() statistics.
+35 -4
View File
@@ -213,42 +213,73 @@ double Flow::mm3_per_mm() const
Flow support_material_flow(const PrintObject *object, float layer_height)
{
// SM Orca: 使用物理挤出机的喷嘴直径
int filament_idx = object->config().support_filament - 1;
int physical_extruder = object->print()->get_physical_extruder(filament_idx);
// SM Orca: 日志 - 配置数组访问边界检查
const auto& nozzle_diameter_config = object->print()->config().nozzle_diameter;
size_t array_size = nozzle_diameter_config.values.size();
return Flow::new_from_config_width(
frSupportMaterial,
// The width parameter accepted by new_from_config_width is of type ConfigOptionFloatOrPercent, the Flow class takes care of the percent to value substitution.
(object->config().support_line_width.value > 0) ? object->config().support_line_width : object->config().line_width,
// if object->config().support_filament == 0 (which means to not trigger tool change, but use the current extruder instead), get_at will return the 0th component.
float(object->print()->config().nozzle_diameter.get_at(object->config().support_filament-1)),
float(object->print()->config().nozzle_diameter.get_at(physical_extruder)),
(layer_height > 0.f) ? layer_height : float(object->config().layer_height.value));
}
//BBS
Flow support_transition_flow(const PrintObject* object)
{
//BBS: support transition of tree support is bridge flow
float dmr = float(object->print()->config().nozzle_diameter.get_at(object->config().support_filament - 1));
// SM Orca: 使用物理挤出机的喷嘴直径
int filament_idx = object->config().support_filament - 1;
int physical_extruder = object->print()->get_physical_extruder(filament_idx);
// SM Orca: 日志 - 配置数组访问边界检查
const auto& nozzle_diameter_config = object->print()->config().nozzle_diameter;
size_t array_size = nozzle_diameter_config.values.size();
float dmr = float(object->print()->config().nozzle_diameter.get_at(physical_extruder));
return Flow::bridging_flow(dmr, dmr);
}
Flow support_material_1st_layer_flow(const PrintObject *object, float layer_height)
{
// SM Orca: 使用物理挤出机的喷嘴直径
int filament_idx = object->config().support_filament - 1;
int physical_extruder = object->print()->get_physical_extruder(filament_idx);
const PrintConfig &print_config = object->print()->config();
// SM Orca: 日志 - 配置数组访问边界检查
size_t array_size = print_config.nozzle_diameter.values.size();
const auto &width = (print_config.initial_layer_line_width.value > 0) ? print_config.initial_layer_line_width : object->config().support_line_width;
return Flow::new_from_config_width(
frSupportMaterial,
// The width parameter accepted by new_from_config_width is of type ConfigOptionFloatOrPercent, the Flow class takes care of the percent to value substitution.
(width.value > 0) ? width : object->config().line_width,
float(print_config.nozzle_diameter.get_at(object->config().support_filament-1)),
float(print_config.nozzle_diameter.get_at(physical_extruder)),
(layer_height > 0.f) ? layer_height : float(print_config.initial_layer_print_height.value));
}
Flow support_material_interface_flow(const PrintObject *object, float layer_height)
{
// SM Orca: 使用物理挤出机的喷嘴直径
int filament_idx = object->config().support_interface_filament - 1;
int physical_extruder = object->print()->get_physical_extruder(filament_idx);
// SM Orca: 日志 - 配置数组访问边界检查
const auto& nozzle_diameter_config = object->print()->config().nozzle_diameter;
size_t array_size = nozzle_diameter_config.values.size();
return Flow::new_from_config_width(
frSupportMaterialInterface,
// The width parameter accepted by new_from_config_width is of type ConfigOptionFloatOrPercent, the Flow class takes care of the percent to value substitution.
(object->config().support_line_width > 0) ? object->config().support_line_width : object->config().line_width,
// if object->config().support_interface_filament == 0 (which means to not trigger tool change, but use the current extruder instead), get_at will return the 0th component.
float(object->print()->config().nozzle_diameter.get_at(object->config().support_interface_filament-1)),
float(object->print()->config().nozzle_diameter.get_at(physical_extruder)),
(layer_height > 0.f) ? layer_height : float(object->config().layer_height.value));
}
+2727 -2635
View File
File diff suppressed because it is too large Load Diff
+8 -1
View File
@@ -114,7 +114,7 @@ private:
std::string append_tcr2(GCode &gcodegen, const WipeTower::ToolChangeResult &tcr, int new_extruder_id, double z = -1.) const;
// Postprocesses gcode: rotates and moves G1 extrusions and returns result
std::string post_process_wipe_tower_moves(const WipeTower::ToolChangeResult& tcr, const Vec2f& translation, float angle) const;
std::string post_process_wipe_tower_moves(GCode& gcodegen, const WipeTower::ToolChangeResult& tcr, const Vec2f& translation, float angle) const;
// Left / right edges of the wipe tower, for the planning of wipe moves.
const float m_left;
const float m_right;
@@ -197,6 +197,13 @@ public:
//BBS: set offset for gcode writer
void set_gcode_offset(double x, double y) { m_writer.set_xy_offset(x, y); m_processor.set_xy_offset(x, y);}
// SM Orca: Set filament-extruder mapping
void set_filament_extruder_map(const std::unordered_map<int, int>& map) {
m_writer.set_filament_extruder_map(map);
m_processor.set_filament_extruder_map(map);
if (m_cooling_buffer) m_cooling_buffer->set_filament_extruder_map(map);
}
// Exported for the helper classes (OozePrevention, Wipe) and for the Perl binding for unit tests.
const Vec2d& origin() const { return m_origin; }
void set_origin(const Vec2d &pointf);
@@ -482,8 +482,11 @@ static inline float get_default_perimeter_spacing(const PrintObject &print_objec
std::vector<unsigned int> printing_extruders = print_object.object_extruders();
assert(!printing_extruders.empty());
float avg_extruder = 0;
for(unsigned int extruder_id : printing_extruders)
avg_extruder += float(scale_(print_object.print()->config().nozzle_diameter.get_at(extruder_id)));
for(unsigned int extruder_id : printing_extruders) {
// SM Orca: nozzle_diameter是物理挤出机参数,使用physical_extruder_id访问
int physical_extruder_id = print_object.print()->get_physical_extruder(extruder_id);
avg_extruder += float(scale_(print_object.print()->config().nozzle_diameter.get_at(physical_extruder_id)));
}
avg_extruder /= printing_extruders.size();
return avg_extruder;
}
+8 -5
View File
@@ -340,12 +340,14 @@ std::vector<PerExtruderAdjustments> CoolingBuffer::parse_layer_gcode(const std::
for (size_t i = 0; i < m_extruder_ids.size(); ++ i) {
PerExtruderAdjustments &adj = per_extruder_adjustments[i];
unsigned int extruder_id = m_extruder_ids[i];
// SM Orca: 冷却参数都是物理挤出机参数(无耗材覆盖),使用physical_extruder_id访问
int physical_extruder_id = get_physical_extruder(extruder_id);
adj.extruder_id = extruder_id;
adj.cooling_slow_down_enabled = m_config.slow_down_for_layer_cooling.get_at(extruder_id);
adj.slow_down_layer_time = float(m_config.slow_down_layer_time.get_at(extruder_id));
adj.slow_down_min_speed = float(m_config.slow_down_min_speed.get_at(extruder_id));
adj.cooling_slow_down_enabled = m_config.slow_down_for_layer_cooling.get_at(physical_extruder_id);
adj.slow_down_layer_time = float(m_config.slow_down_layer_time.get_at(physical_extruder_id));
adj.slow_down_min_speed = float(m_config.slow_down_min_speed.get_at(physical_extruder_id));
// ORCA: To enable dont slow down external perimeters feature per filament (extruder)
adj.dont_slow_down_outer_wall = m_config.dont_slow_down_outer_wall.get_at(extruder_id);
adj.dont_slow_down_outer_wall = m_config.dont_slow_down_outer_wall.get_at(physical_extruder_id);
map_extruder_to_per_extruder_adjustment[extruder_id] = i;
}
@@ -731,7 +733,8 @@ std::string CoolingBuffer::apply_layer_cooldown(
&supp_interface_fan_control, &supp_interface_fan_speed,
&ironing_fan_control, &ironing_fan_speed
](bool immediately_apply) {
#define EXTRUDER_CONFIG(OPT) m_config.OPT.get_at(m_current_extruder)
// SM Orca: 风扇参数都是物理挤出机参数(无耗材覆盖),使用physical_extruder_id访问
#define EXTRUDER_CONFIG(OPT) m_config.OPT.get_at(get_physical_extruder(m_current_extruder))
float fan_min_speed = EXTRUDER_CONFIG(fan_min_speed);
float fan_speed_new = EXTRUDER_CONFIG(reduce_fan_stop_start_freq) ? fan_min_speed : 0;
//BBS
+9
View File
@@ -27,6 +27,13 @@ public:
void reset(const Vec3d &position);
void set_current_extruder(unsigned int extruder_id) { m_current_extruder = extruder_id; }
std::string process_layer(std::string &&gcode, size_t layer_id, bool flush);
// SM Orca: Set filament to physical extruder mapping for correct parameter access
void set_filament_extruder_map(const std::unordered_map<int, int>& map) { m_filament_extruder_map = map; }
// SM Orca: Get physical extruder ID from filament ID
int get_physical_extruder(int filament_idx) const {
auto it = m_filament_extruder_map.find(filament_idx);
return (it != m_filament_extruder_map.end()) ? it->second : filament_idx;
}
private:
CoolingBuffer& operator=(const CoolingBuffer&) = delete;
@@ -55,6 +62,8 @@ private:
// the PrintConfig slice of FullPrintConfig is constant, thus no thread synchronization is required.
const PrintConfig &m_config;
unsigned int m_current_extruder;
// SM Orca: Filament to physical extruder mapping for correct parameter access
std::unordered_map<int, int> m_filament_extruder_map;
//BBS: current fan speed
int m_current_fan_speed;
};
+463 -70
View File
@@ -94,7 +94,6 @@ const std::vector<std::string> GCodeProcessor::Reserved_Tags_compatible = {
" PA_CHANGE:"
};
const std::string GCodeProcessor::Flush_Start_Tag = " FLUSH_START";
const std::string GCodeProcessor::Flush_End_Tag = " FLUSH_END";
@@ -397,7 +396,6 @@ void GCodeProcessor::TimeProcessor::reset()
filament_unload_times = 0.0f;
machine_tool_change_time = 0.0f;
for (size_t i = 0; i < static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Count); ++i) {
machines[i].reset();
}
@@ -457,7 +455,6 @@ void GCodeProcessor::UsedFilaments::process_color_change_cache()
}
}
void GCodeProcessor::UsedFilaments::process_total_volume_cache(GCodeProcessor* processor)
{
size_t active_extruder_id = processor->m_extruder_id;
@@ -526,9 +523,17 @@ void GCodeProcessor::UsedFilaments::process_role_cache(GCodeProcessor* processor
if (role_cache != 0.0f) {
std::pair<double, double> filament = { 0.0f, 0.0f };
double s = PI * sqr(0.5 * processor->m_result.filament_diameters[processor->m_extruder_id]);
float diameter = (static_cast<size_t>(processor->m_extruder_id) < processor->m_result.filament_diameters.size())
? processor->m_result.filament_diameters[processor->m_extruder_id]
: processor->m_result.filament_diameters.back();
float density = (static_cast<size_t>(processor->m_extruder_id) < processor->m_result.filament_densities.size())
? processor->m_result.filament_densities[processor->m_extruder_id]
: processor->m_result.filament_densities.back();
double s = PI * sqr(0.5 * diameter);
filament.first = role_cache / s * 0.001;
filament.second = role_cache * processor->m_result.filament_densities[processor->m_extruder_id] * 0.001;
filament.second = role_cache * density * 0.001;
ExtrusionRole active_role = processor->m_extrusion_role;
if (filaments_per_role.find(active_role) != filaments_per_role.end()) {
@@ -556,6 +561,20 @@ void GCodeProcessorResult::reset() {
//BBS: add mutex for protection of gcode result
lock();
size_t saved_count = extruders_count;
if (saved_count == 0 || saved_count > 256) {
// 尝试从已有数组大小推断(优先使用filament_diameters的大小)
if (!filament_diameters.empty() && filament_diameters.size() <= 256) {
saved_count = filament_diameters.size();
<< saved_count;
} else {
// 对于只有少量耗材的用户,稍微多分配一些内存影响很小
saved_count = 16;
<< saved_count << " (was " << extruders_count << ", array size was " << filament_diameters.size() << ")";
}
}
moves = std::vector<GCodeProcessorResult::MoveVertex>();
printable_area = Pointfs();
//BBS: add bed exclude area
@@ -567,10 +586,19 @@ void GCodeProcessorResult::reset() {
timelapse_warning_code = 0;
printable_height = 0.0f;
settings_ids.reset();
extruders_count = 0;
extruders_count = saved_count;
extruder_colors = std::vector<std::string>();
filament_diameters = std::vector<float>(MIN_EXTRUDERS_COUNT, DEFAULT_FILAMENT_DIAMETER);
filament_densities = std::vector<float>(MIN_EXTRUDERS_COUNT, DEFAULT_FILAMENT_DENSITY);
filament_diameters = std::vector<float>(saved_count, DEFAULT_FILAMENT_DIAMETER);
filament_densities = std::vector<float>(saved_count, DEFAULT_FILAMENT_DENSITY);
filament_costs = std::vector<float>(saved_count, DEFAULT_FILAMENT_COST);
required_nozzle_HRC = std::vector<int>(saved_count, DEFAULT_FILAMENT_HRC);
filament_vitrification_temperature = std::vector<int>(saved_count, DEFAULT_FILAMENT_VITRIFICATION_TEMPERATURE);
<< " (original extruders_count=" << (saved_count == extruders_count ? "preserved" : "inferred")
<< ", this=" << this << ")";
custom_gcode_per_print_z = std::vector<CustomGCode::Item>();
spiral_vase_layers = std::vector<std::pair<float, std::pair<size_t, size_t>>>();
time = 0;
@@ -583,6 +611,18 @@ void GCodeProcessorResult::reset() {
//BBS: add mutex for protection of gcode result
lock();
size_t saved_count = extruders_count;
if (saved_count == 0 || saved_count > 256) {
// 尝试从已有数组大小推断(优先使用filament_diameters的大小)
if (!filament_diameters.empty() && filament_diameters.size() <= 256) {
saved_count = filament_diameters.size();
} else {
// 对于只有少量耗材的用户,稍微多分配一些内存影响很小
saved_count = 16;
}
}
moves.clear();
lines_ends.clear();
printable_area = Pointfs();
@@ -596,13 +636,17 @@ void GCodeProcessorResult::reset() {
timelapse_warning_code = 0;
printable_height = 0.0f;
settings_ids.reset();
extruders_count = 0;
backtrace_enabled = false;
extruder_colors = std::vector<std::string>();
filament_diameters = std::vector<float>(MIN_EXTRUDERS_COUNT, DEFAULT_FILAMENT_DIAMETER);
required_nozzle_HRC = std::vector<int>(MIN_EXTRUDERS_COUNT, DEFAULT_FILAMENT_HRC);
filament_densities = std::vector<float>(MIN_EXTRUDERS_COUNT, DEFAULT_FILAMENT_DENSITY);
filament_costs = std::vector<float>(MIN_EXTRUDERS_COUNT, DEFAULT_FILAMENT_COST);
extruders_count = saved_count;
filament_diameters = std::vector<float>(saved_count, DEFAULT_FILAMENT_DIAMETER);
required_nozzle_HRC = std::vector<int>(saved_count, DEFAULT_FILAMENT_HRC);
filament_densities = std::vector<float>(saved_count, DEFAULT_FILAMENT_DENSITY);
filament_costs = std::vector<float>(saved_count, DEFAULT_FILAMENT_COST);
filament_vitrification_temperature = std::vector<int>(saved_count, DEFAULT_FILAMENT_VITRIFICATION_TEMPERATURE);
custom_gcode_per_print_z = std::vector<CustomGCode::Item>();
spiral_vase_layers = std::vector<std::pair<float, std::pair<size_t, size_t>>>();
bed_match_result = BedMatchResult(true);
@@ -719,6 +763,40 @@ void GCodeProcessor::apply_config(const PrintConfig& config)
m_preheat_steps = 1;
m_result.backtrace_enabled = m_preheat_time > 0 && (m_is_XL_printer || (!m_single_extruder_multi_material && extruders_count > 1));
size_t physical_extruder_count = config.extruder_offset.values.size();
// 验证各配置数组大小
if (config.filament_density.values.size() < extruders_count) {
BOOST_LOG_TRIVIAL(warning) << "SM Orca: filament_density has "
<< config.filament_density.values.size() << " values, expected " << extruders_count
<< " (will use fallback values)";
}
if (config.filament_cost.values.size() < extruders_count) {
BOOST_LOG_TRIVIAL(warning) << "SM Orca: filament_cost has "
<< config.filament_cost.values.size() << " values, expected " << extruders_count
<< " (will use fallback values)";
}
if (config.nozzle_temperature.values.size() < extruders_count) {
BOOST_LOG_TRIVIAL(warning) << "SM Orca: nozzle_temperature has "
<< config.nozzle_temperature.values.size() << " values, expected " << extruders_count
<< " (will use fallback values)";
}
// 验证映射表有效性
if (!m_filament_extruder_map.empty()) {
for (size_t i = 0; i < extruders_count; ++i) {
int physical_extruder = get_physical_extruder(i);
if (physical_extruder < 0 ||
physical_extruder >= static_cast<int>(physical_extruder_count)) {
BOOST_LOG_TRIVIAL(error) << "SM Orca: Filament " << i
<< " maps to invalid physical extruder " << physical_extruder
<< " (valid range: 0-" << (physical_extruder_count - 1) << ")";
}
}
}
m_extruder_offsets.resize(extruders_count);
m_extruder_colors.resize(extruders_count);
m_result.filament_diameters.resize(extruders_count);
@@ -731,20 +809,94 @@ void GCodeProcessor::apply_config(const PrintConfig& config)
m_extruder_temps_first_layer_config.resize(extruders_count);
m_result.nozzle_hrc = static_cast<int>(config.nozzle_hrc.getInt());
m_result.nozzle_type = config.nozzle_type;
size_t diameter_count = config.filament_diameter.values.size();
size_t density_count = config.filament_density.values.size();
size_t cost_count = config.filament_cost.values.size();
size_t temp_initial_count = config.nozzle_temperature_initial_layer.values.size();
size_t temp_count = config.nozzle_temperature.values.size();
size_t hrc_count = config.required_nozzle_HRC.values.size();
size_t vitrification_count = config.temperature_vitrification.values.size();
for (size_t i = 0; i < extruders_count; ++ i) {
m_extruder_offsets[i] = to_3d(config.extruder_offset.get_at(i).cast<float>().eval(), 0.f);
int physical_extruder = get_physical_extruder(i);
if (physical_extruder < 0 || physical_extruder >= static_cast<int>(physical_extruder_count)) {
BOOST_LOG_TRIVIAL(error) << "SM Orca: Filament " << i
<< " maps to invalid physical extruder " << physical_extruder
<< " (valid range: 0-" << (physical_extruder_count - 1) << ")";
// 使用filament index作为fallback1:1映射),如果也越界则使用0
physical_extruder = (i < physical_extruder_count) ? static_cast<int>(i) : 0;
BOOST_LOG_TRIVIAL(error) << " SM Orca: Using fallback physical extruder " << physical_extruder;
}
m_extruder_offsets[i] = to_3d(config.extruder_offset.get_at(physical_extruder).cast<float>().eval(), 0.f);
m_extruder_colors[i] = static_cast<unsigned char>(i);
m_extruder_temps_first_layer_config[i] = static_cast<int>(config.nozzle_temperature_initial_layer.get_at(i));
m_extruder_temps_config[i] = static_cast<int>(config.nozzle_temperature.get_at(i));
// 温度是挤出机属性,使用 physical_extruder 而不是 filament index
if (physical_extruder < static_cast<int>(temp_initial_count)) {
m_extruder_temps_first_layer_config[i] = static_cast<int>(config.nozzle_temperature_initial_layer.get_at(physical_extruder));
} else {
int fallback = temp_initial_count > 0 ?
static_cast<int>(config.nozzle_temperature_initial_layer.get_at(temp_initial_count - 1)) : 210;
m_extruder_temps_first_layer_config[i] = fallback;
BOOST_LOG_TRIVIAL(warning) << "SM Orca: Filament " << i << " (physical extruder " << physical_extruder << ") initial layer temperature not configured, using " << fallback;
}
if (physical_extruder < static_cast<int>(temp_count)) {
m_extruder_temps_config[i] = static_cast<int>(config.nozzle_temperature.get_at(physical_extruder));
} else {
int fallback = temp_count > 0 ?
static_cast<int>(config.nozzle_temperature.get_at(temp_count - 1)) : 210;
m_extruder_temps_config[i] = fallback;
BOOST_LOG_TRIVIAL(warning) << "SM Orca: Filament " << i << " (physical extruder " << physical_extruder << ") temperature not configured, using " << fallback;
}
if (m_extruder_temps_config[i] == 0) {
// This means the value should be ignored and first layer temp should be used.
m_extruder_temps_config[i] = m_extruder_temps_first_layer_config[i];
}
m_result.filament_diameters[i] = static_cast<float>(config.filament_diameter.get_at(i));
m_result.required_nozzle_HRC[i] = static_cast<int>(config.required_nozzle_HRC.get_at(i));
m_result.filament_densities[i] = static_cast<float>(config.filament_density.get_at(i));
m_result.filament_vitrification_temperature[i] = static_cast<float>(config.temperature_vitrification.get_at(i));
m_result.filament_costs[i] = static_cast<float>(config.filament_cost.get_at(i));
if (i < diameter_count) {
m_result.filament_diameters[i] = static_cast<float>(config.filament_diameter.get_at(i));
} else {
float fallback = diameter_count > 0 ?
static_cast<float>(config.filament_diameter.get_at(diameter_count - 1)) : 1.75f;
m_result.filament_diameters[i] = fallback;
BOOST_LOG_TRIVIAL(warning) << "SM Orca: Filament " << i << " diameter not configured, using " << fallback << "mm";
}
if (i < hrc_count) {
m_result.required_nozzle_HRC[i] = static_cast<int>(config.required_nozzle_HRC.get_at(i));
} else {
int fallback = hrc_count > 0 ?
static_cast<int>(config.required_nozzle_HRC.get_at(hrc_count - 1)) : 0;
m_result.required_nozzle_HRC[i] = fallback;
}
if (i < density_count) {
m_result.filament_densities[i] = static_cast<float>(config.filament_density.get_at(i));
} else {
float fallback = density_count > 0 ?
static_cast<float>(config.filament_density.get_at(density_count - 1)) : 1.25f;
m_result.filament_densities[i] = fallback;
BOOST_LOG_TRIVIAL(warning) << "SM Orca: Filament " << i << " density not configured, using " << fallback << " g/cm³";
}
if (i < vitrification_count) {
m_result.filament_vitrification_temperature[i] = static_cast<int>(config.temperature_vitrification.get_at(i));
} else {
int fallback = vitrification_count > 0 ?
static_cast<int>(config.temperature_vitrification.get_at(vitrification_count - 1)) : 0;
m_result.filament_vitrification_temperature[i] = fallback;
}
if (i < cost_count) {
m_result.filament_costs[i] = static_cast<float>(config.filament_cost.get_at(i));
} else {
m_result.filament_costs[i] = 0.0f;
BOOST_LOG_TRIVIAL(warning) << "SM Orca: Filament " << i << " cost not configured, using 0.0";
}
}
if (m_flavor == gcfMarlinLegacy || m_flavor == gcfMarlinFirmware || m_flavor == gcfKlipper || m_flavor == gcfRepRapFirmware) {
@@ -858,24 +1010,35 @@ void GCodeProcessor::apply_config(const DynamicPrintConfig& config)
const ConfigOptionFloats* filament_diameters = config.option<ConfigOptionFloats>("filament_diameter");
if (filament_diameters != nullptr) {
m_result.filament_diameters.clear();
m_result.filament_diameters.resize(filament_diameters->values.size());
for (size_t i = 0; i < filament_diameters->values.size(); ++i) {
size_t config_size = filament_diameters->values.size();
if (m_result.filament_diameters.size() < m_result.extruders_count) {
m_result.filament_diameters.resize(m_result.extruders_count, DEFAULT_FILAMENT_DIAMETER);
}
for (size_t i = 0; i < config_size && i < m_result.extruders_count; ++i) {
m_result.filament_diameters[i] = static_cast<float>(filament_diameters->values[i]);
}
if (config_size > 0 && config_size < m_result.extruders_count) {
float last_value = static_cast<float>(filament_diameters->values[config_size - 1]);
for (size_t i = config_size; i < m_result.extruders_count; ++i) {
m_result.filament_diameters[i] = last_value;
BOOST_LOG_TRIVIAL(debug) << "SM Orca: Filament " << i
<< " diameter not in config, using last value " << last_value << "mm";
}
}
}
if (m_result.filament_diameters.size() < m_result.extruders_count) {
for (size_t i = m_result.filament_diameters.size(); i < m_result.extruders_count; ++i) {
m_result.filament_diameters.emplace_back(DEFAULT_FILAMENT_DIAMETER);
}
m_result.filament_diameters.resize(m_result.extruders_count, DEFAULT_FILAMENT_DIAMETER);
}
const ConfigOptionInts *filament_HRC = config.option<ConfigOptionInts>("required_nozzle_HRC");
if (filament_HRC != nullptr) {
m_result.required_nozzle_HRC.clear();
m_result.required_nozzle_HRC.resize(filament_HRC->values.size());
for (size_t i = 0; i < filament_HRC->values.size(); ++i) { m_result.required_nozzle_HRC[i] = static_cast<float>(filament_HRC->values[i]); }
for (size_t i = 0; i < filament_HRC->values.size(); ++i) { m_result.required_nozzle_HRC[i] = static_cast<int>(filament_HRC->values[i]); }
}
if (m_result.required_nozzle_HRC.size() < m_result.extruders_count) {
@@ -885,43 +1048,75 @@ void GCodeProcessor::apply_config(const DynamicPrintConfig& config)
const ConfigOptionFloats* filament_densities = config.option<ConfigOptionFloats>("filament_density");
if (filament_densities != nullptr) {
m_result.filament_densities.clear();
m_result.filament_densities.resize(filament_densities->values.size());
for (size_t i = 0; i < filament_densities->values.size(); ++i) {
size_t config_size = filament_densities->values.size();
if (m_result.filament_densities.size() < m_result.extruders_count) {
m_result.filament_densities.resize(m_result.extruders_count, DEFAULT_FILAMENT_DENSITY);
}
for (size_t i = 0; i < config_size && i < m_result.extruders_count; ++i) {
m_result.filament_densities[i] = static_cast<float>(filament_densities->values[i]);
}
if (config_size > 0 && config_size < m_result.extruders_count) {
float last_value = static_cast<float>(filament_densities->values[config_size - 1]);
for (size_t i = config_size; i < m_result.extruders_count; ++i) {
m_result.filament_densities[i] = last_value;
BOOST_LOG_TRIVIAL(debug) << "SM Orca: Filament " << i
<< " density not in config, using last value " << last_value << " g/cm³";
}
}
}
if (m_result.filament_densities.size() < m_result.extruders_count) {
for (size_t i = m_result.filament_densities.size(); i < m_result.extruders_count; ++i) {
m_result.filament_densities.emplace_back(DEFAULT_FILAMENT_DENSITY);
}
m_result.filament_densities.resize(m_result.extruders_count, DEFAULT_FILAMENT_DENSITY);
}
//BBS
const ConfigOptionFloats* filament_costs = config.option<ConfigOptionFloats>("filament_cost");
if (filament_costs != nullptr) {
m_result.filament_costs.clear();
m_result.filament_costs.resize(filament_costs->values.size());
for (size_t i = 0; i < filament_costs->values.size(); ++i)
m_result.filament_costs[i]=static_cast<float>(filament_costs->values[i]);
size_t config_size = filament_costs->values.size();
if (m_result.filament_costs.size() < m_result.extruders_count) {
m_result.filament_costs.resize(m_result.extruders_count, DEFAULT_FILAMENT_COST);
}
for (size_t i = 0; i < config_size && i < m_result.extruders_count; ++i)
m_result.filament_costs[i] = static_cast<float>(filament_costs->values[i]);
if (config_size < m_result.extruders_count) {
for (size_t i = config_size; i < m_result.extruders_count; ++i) {
m_result.filament_costs[i] = DEFAULT_FILAMENT_COST;
BOOST_LOG_TRIVIAL(debug) << "SM Orca: Filament " << i
<< " cost not in config, using default " << DEFAULT_FILAMENT_COST;
}
}
}
for (size_t i = m_result.filament_costs.size(); i < m_result.extruders_count; ++i) {
m_result.filament_costs.emplace_back(DEFAULT_FILAMENT_COST);
if (m_result.filament_costs.size() < m_result.extruders_count) {
m_result.filament_costs.resize(m_result.extruders_count, DEFAULT_FILAMENT_COST);
}
//BBS
const ConfigOptionInts* filament_vitrification_temperature = config.option<ConfigOptionInts>("temperature_vitrification");
if (filament_vitrification_temperature != nullptr) {
m_result.filament_vitrification_temperature.clear();
m_result.filament_vitrification_temperature.resize(filament_vitrification_temperature->values.size());
for (size_t i = 0; i < filament_vitrification_temperature->values.size(); ++i) {
size_t config_size = filament_vitrification_temperature->values.size();
if (m_result.filament_vitrification_temperature.size() < m_result.extruders_count) {
m_result.filament_vitrification_temperature.resize(m_result.extruders_count, DEFAULT_FILAMENT_VITRIFICATION_TEMPERATURE);
}
for (size_t i = 0; i < config_size && i < m_result.extruders_count; ++i) {
m_result.filament_vitrification_temperature[i] = static_cast<int>(filament_vitrification_temperature->values[i]);
}
}
if (m_result.filament_vitrification_temperature.size() < m_result.extruders_count) {
for (size_t i = m_result.filament_vitrification_temperature.size(); i < m_result.extruders_count; ++i) {
m_result.filament_vitrification_temperature.emplace_back(DEFAULT_FILAMENT_VITRIFICATION_TEMPERATURE);
if (config_size > 0 && config_size < m_result.extruders_count) {
int last_value = static_cast<int>(filament_vitrification_temperature->values[config_size - 1]);
for (size_t i = config_size; i < m_result.extruders_count; ++i) {
m_result.filament_vitrification_temperature[i] = last_value;
BOOST_LOG_TRIVIAL(debug) << "SM Orca: Filament " << i
<< " vitrification temperature not in config, using last value " << last_value;
}
}
}
@@ -937,8 +1132,13 @@ void GCodeProcessor::apply_config(const DynamicPrintConfig& config)
}
}
else {
m_extruder_offsets.resize(extruder_offset->values.size());
for (size_t i = 0; i < extruder_offset->values.size(); ++i) {
size_t physical_count = extruder_offset->values.size();
if (m_extruder_offsets.size() < m_result.extruders_count) {
m_extruder_offsets.resize(m_result.extruders_count, DEFAULT_EXTRUDER_OFFSET);
}
// 只更新物理挤出机的offset
for (size_t i = 0; i < physical_count && i < m_extruder_offsets.size(); ++i) {
Vec2f offset = extruder_offset->values[i].cast<float>();
m_extruder_offsets[i] = { offset(0), offset(1), 0.0f };
}
@@ -946,8 +1146,18 @@ void GCodeProcessor::apply_config(const DynamicPrintConfig& config)
}
if (m_extruder_offsets.size() < m_result.extruders_count) {
size_t physical_count = m_extruder_offsets.size();
for (size_t i = m_extruder_offsets.size(); i < m_result.extruders_count; ++i) {
m_extruder_offsets.emplace_back(DEFAULT_EXTRUDER_OFFSET);
int physical_extruder = get_physical_extruder(i);
// 如果映射的物理挤出机索引在有效范围内,复用它的offset
if (physical_extruder >= 0 && physical_extruder < static_cast<int>(physical_count)) {
m_extruder_offsets.emplace_back(m_extruder_offsets[physical_extruder]);
BOOST_LOG_TRIVIAL(debug) << "Filament " << i << " using offset from physical extruder " << physical_extruder;
} else {
// 否则使用默认offset
m_extruder_offsets.emplace_back(DEFAULT_EXTRUDER_OFFSET);
BOOST_LOG_TRIVIAL(warning) << "Filament " << i << " using default offset (physical extruder " << physical_extruder << " out of range)";
}
}
}
@@ -991,7 +1201,6 @@ void GCodeProcessor::apply_config(const DynamicPrintConfig& config)
if (machine_tool_change_time != nullptr)
m_time_processor.machine_tool_change_time = static_cast<float>(machine_tool_change_time->value);
if (m_flavor == gcfMarlinLegacy || m_flavor == gcfMarlinFirmware || m_flavor == gcfKlipper) {
const ConfigOptionFloats* machine_max_acceleration_x = config.option<ConfigOptionFloats>("machine_max_acceleration_x");
if (machine_max_acceleration_x != nullptr)
@@ -1053,7 +1262,6 @@ void GCodeProcessor::apply_config(const DynamicPrintConfig& config)
if (machine_max_acceleration_retracting != nullptr)
m_time_processor.machine_limits.machine_max_acceleration_retracting.values = machine_max_acceleration_retracting->values;
// Legacy Marlin does not have separate travel acceleration, it uses the 'extruding' value instead.
const ConfigOptionFloats* machine_max_acceleration_travel = config.option<ConfigOptionFloats>(m_flavor == gcfMarlinLegacy || m_flavor == gcfKlipper
? "machine_max_acceleration_extruding"
@@ -1061,7 +1269,6 @@ void GCodeProcessor::apply_config(const DynamicPrintConfig& config)
if (machine_max_acceleration_travel != nullptr)
m_time_processor.machine_limits.machine_max_acceleration_travel.values = machine_max_acceleration_travel->values;
const ConfigOptionFloats* machine_min_extruding_rate = config.option<ConfigOptionFloats>("machine_min_extruding_rate");
if (machine_min_extruding_rate != nullptr)
m_time_processor.machine_limits.machine_min_extruding_rate.values = machine_min_extruding_rate->values;
@@ -1113,10 +1320,32 @@ void GCodeProcessor::apply_config(const DynamicPrintConfig& config)
if (bed_type != nullptr)
m_result.bed_type = (BedType)bed_type->value;
const ConfigOptionFloat* z_offset = config.option<ConfigOptionFloat>("z_offset");
if (z_offset != nullptr)
m_z_offset = z_offset->value;
bool arrays_valid = true;
if (m_result.filament_diameters.size() != m_result.extruders_count) {
BOOST_LOG_TRIVIAL(error) << "SM Orca: CRITICAL - filament_diameters size mismatch: "
<< m_result.filament_diameters.size() << " != " << m_result.extruders_count;
arrays_valid = false;
}
if (m_result.filament_densities.size() != m_result.extruders_count) {
BOOST_LOG_TRIVIAL(error) << "SM Orca: CRITICAL - filament_densities size mismatch: "
<< m_result.filament_densities.size() << " != " << m_result.extruders_count;
arrays_valid = false;
}
if (m_result.filament_costs.size() != m_result.extruders_count) {
BOOST_LOG_TRIVIAL(error) << "SM Orca: CRITICAL - filament_costs size mismatch: "
<< m_result.filament_costs.size() << " != " << m_result.extruders_count;
arrays_valid = false;
}
if (m_result.filament_vitrification_temperature.size() != m_result.extruders_count) {
BOOST_LOG_TRIVIAL(error) << "SM Orca: CRITICAL - filament_vitrification_temperature size mismatch: "
<< m_result.filament_vitrification_temperature.size() << " != " << m_result.extruders_count;
arrays_valid = false;
}
}
void GCodeProcessor::enable_stealth_time_estimator(bool enabled)
@@ -1556,6 +1785,22 @@ void GCodeProcessor::process_gcode_line(const GCodeReader::GCodeLine& line, bool
// update start position
m_start_position = m_end_position;
if (std::isnan(m_start_position[X]) || std::isinf(m_start_position[X]) ||
std::isnan(m_start_position[Y]) || std::isinf(m_start_position[Y]) ||
std::isnan(m_start_position[Z]) || std::isinf(m_start_position[Z])) {
BOOST_LOG_TRIVIAL(error) << "SM Orca: Detected invalid m_start_position at line " << m_line_id
<< " extruder=" << static_cast<int>(m_extruder_id)
<< " m_start_position=(" << m_start_position[X] << ", " << m_start_position[Y] << ", " << m_start_position[Z] << ")"
<< " m_end_position=(" << m_end_position[X] << ", " << m_end_position[Y] << ", " << m_end_position[Z] << ")";
// 重置为原点,防止污染继续传播
m_start_position[X] = std::isnan(m_start_position[X]) || std::isinf(m_start_position[X]) ? 0.0f : m_start_position[X];
m_start_position[Y] = std::isnan(m_start_position[Y]) || std::isinf(m_start_position[Y]) ? 0.0f : m_start_position[Y];
m_start_position[Z] = std::isnan(m_start_position[Z]) || std::isinf(m_start_position[Z]) ? 0.0f : m_start_position[Z];
m_end_position[X] = std::isnan(m_end_position[X]) || std::isinf(m_end_position[X]) ? 0.0f : m_end_position[X];
m_end_position[Y] = std::isnan(m_end_position[Y]) || std::isinf(m_end_position[Y]) ? 0.0f : m_end_position[Y];
m_end_position[Z] = std::isnan(m_end_position[Z]) || std::isinf(m_end_position[Z]) ? 0.0f : m_end_position[Z];
}
const std::string_view cmd = line.cmd();
if (m_flavor == gcfKlipper)
{
@@ -2635,6 +2880,21 @@ void GCodeProcessor::process_G1(const GCodeReader::GCodeLine& line, const std::o
m_end_position[a] = absolute_position((Axis)a, line);
}
if (std::isnan(m_end_position[X]) || std::isinf(m_end_position[X]) ||
std::isnan(m_end_position[Y]) || std::isinf(m_end_position[Y]) ||
std::isnan(m_end_position[Z]) || std::isinf(m_end_position[Z])) {
BOOST_LOG_TRIVIAL(error) << "SM Orca: Invalid m_end_position after G1 processing for extruder " << static_cast<int>(m_extruder_id)
<< " m_end_position=(" << m_end_position[X] << ", " << m_end_position[Y] << ", " << m_end_position[Z] << ")"
<< " m_start_position=(" << m_start_position[X] << ", " << m_start_position[Y] << ", " << m_start_position[Z] << ")"
<< " m_origin=(" << m_origin[X] << ", " << m_origin[Y] << ", " << m_origin[Z] << ")"
<< " has_X=" << line.has(X) << " has_Y=" << line.has(Y) << " has_Z=" << line.has(Z)
<< " X_value=" << (line.has(X) ? line.value(X) : 0.0f)
<< " Y_value=" << (line.has(Y) ? line.value(Y) : 0.0f)
<< " Z_value=" << (line.has(Z) ? line.value(Z) : 0.0f)
<< " positioning=" << (m_global_positioning_type == EPositioningType::Relative ? "relative" : "absolute")
<< " units=" << (m_units == EUnits::Inches ? "inches" : "mm");
}
// updates feedrate from line, if present
if (line.has_f())
m_feedrate = line.f() * MMMIN_TO_MMSEC;
@@ -2698,13 +2958,32 @@ void GCodeProcessor::process_G1(const GCodeReader::GCodeLine& line, const std::o
else if (m_extrusion_role == erExternalPerimeter)
// cross section: rectangle
m_width = delta_pos[E] * static_cast<float>(M_PI * sqr(1.05f * filament_radius)) / (delta_xyz * m_height);
else if (m_extrusion_role == erBridgeInfill || m_extrusion_role == erInternalBridgeInfill || m_extrusion_role == erNone)
else if (m_extrusion_role == erBridgeInfill || m_extrusion_role == erInternalBridgeInfill || m_extrusion_role == erNone) {
float diameter = (static_cast<size_t>(m_extruder_id) < m_result.filament_diameters.size())
? m_result.filament_diameters[m_extruder_id]
: m_result.filament_diameters.back();
float ratio = delta_pos[E] / delta_xyz;
if (ratio < 0.0f) {
BOOST_LOG_TRIVIAL(warning) << "SM Orca: Negative E/XYZ ratio (" << ratio
<< ") for extruder " << m_extruder_id << ", using absolute value";
ratio = std::abs(ratio);
}
// cross section: circle
m_width = static_cast<float>(m_result.filament_diameters[m_extruder_id]) * std::sqrt(delta_pos[E] / delta_xyz);
m_width = diameter * std::sqrt(ratio);
}
else
// cross section: rectangle + 2 semicircles
m_width = delta_pos[E] * static_cast<float>(M_PI * sqr(filament_radius)) / (delta_xyz * m_height) + static_cast<float>(1.0 - 0.25 * M_PI) * m_height;
if (std::isnan(m_width) || std::isinf(m_width)) {
BOOST_LOG_TRIVIAL(error) << "SM Orca: Invalid width calculated: " << m_width
<< " for extruder " << m_extruder_id
<< " (E=" << delta_pos[E] << ", XYZ=" << delta_xyz << ")";
m_width = DEFAULT_TOOLPATH_WIDTH;
}
if (m_width == 0.0f)
m_width = DEFAULT_TOOLPATH_WIDTH;
@@ -2913,7 +3192,6 @@ void GCodeProcessor::process_G1(const GCodeReader::GCodeLine& line, const std::o
// axis reversal
std::max(-v_exit, v_entry));
float axis_max_jerk = get_axis_max_jerk(static_cast<PrintEstimatedStatistics::ETimeMode>(i), static_cast<Axis>(a));
if (jerk > axis_max_jerk) {
v_factor *= axis_max_jerk / jerk;
@@ -3086,6 +3364,22 @@ void GCodeProcessor::process_G2_G3(const GCodeReader::GCodeLine& line)
for (unsigned char a = X; a <= E; ++a) {
m_end_position[a] = absolute_position((Axis)a, line);
}
if (std::isnan(m_end_position[X]) || std::isinf(m_end_position[X]) ||
std::isnan(m_end_position[Y]) || std::isinf(m_end_position[Y]) ||
std::isnan(m_end_position[Z]) || std::isinf(m_end_position[Z])) {
BOOST_LOG_TRIVIAL(error) << "SM Orca: Invalid m_end_position after G2/G3 processing for extruder " << static_cast<int>(m_extruder_id)
<< " m_end_position=(" << m_end_position[X] << ", " << m_end_position[Y] << ", " << m_end_position[Z] << ")"
<< " m_start_position=(" << m_start_position[X] << ", " << m_start_position[Y] << ", " << m_start_position[Z] << ")"
<< " m_origin=(" << m_origin[X] << ", " << m_origin[Y] << ", " << m_origin[Z] << ")"
<< " has_X=" << line.has(X) << " has_Y=" << line.has(Y) << " has_Z=" << line.has(Z)
<< " X_value=" << (line.has(X) ? line.value(X) : 0.0f)
<< " Y_value=" << (line.has(Y) ? line.value(Y) : 0.0f)
<< " Z_value=" << (line.has(Z) ? line.value(Z) : 0.0f)
<< " positioning=" << (m_global_positioning_type == EPositioningType::Relative ? "relative" : "absolute")
<< " units=" << (m_units == EUnits::Inches ? "inches" : "mm");
}
//BBS: G2 G3 line but has no I and J axis, invalid G code format
if (!line.has(I) && !line.has(J))
return;
@@ -3130,7 +3424,6 @@ void GCodeProcessor::process_G2_G3(const GCodeReader::GCodeLine& line)
EMoveType type = move_type(delta_pos[E]);
const float delta_xyz = std::sqrt(sqr(arc_length) + sqr(delta_pos[Z]));
m_travel_dist = delta_xyz;
if (type == EMoveType::Extrude) {
@@ -3178,13 +3471,32 @@ void GCodeProcessor::process_G2_G3(const GCodeReader::GCodeLine& line)
else if (m_extrusion_role == erExternalPerimeter)
//BBS: cross section: rectangle
m_width = delta_pos[E] * static_cast<float>(M_PI * sqr(1.05f * filament_radius)) / (delta_xyz * m_height);
else if (m_extrusion_role == erBridgeInfill || m_extrusion_role == erInternalBridgeInfill || m_extrusion_role == erNone)
else if (m_extrusion_role == erBridgeInfill || m_extrusion_role == erInternalBridgeInfill || m_extrusion_role == erNone) {
float diameter = (static_cast<size_t>(m_extruder_id) < m_result.filament_diameters.size())
? m_result.filament_diameters[m_extruder_id]
: m_result.filament_diameters.back();
float ratio = delta_pos[E] / delta_xyz;
if (ratio < 0.0f) {
BOOST_LOG_TRIVIAL(warning) << "SM Orca: Negative E/XYZ ratio (" << ratio
<< ") for extruder " << m_extruder_id << ", using absolute value";
ratio = std::abs(ratio);
}
//BBS: cross section: circle
m_width = static_cast<float>(m_result.filament_diameters[m_extruder_id]) * std::sqrt(delta_pos[E] / delta_xyz);
m_width = diameter * std::sqrt(ratio);
}
else
//BBS: cross section: rectangle + 2 semicircles
m_width = delta_pos[E] * static_cast<float>(M_PI * sqr(filament_radius)) / (delta_xyz * m_height) + static_cast<float>(1.0 - 0.25 * M_PI) * m_height;
if (std::isnan(m_width) || std::isinf(m_width)) {
BOOST_LOG_TRIVIAL(error) << "SM Orca: Invalid width calculated: " << m_width
<< " for extruder " << m_extruder_id
<< " (E=" << delta_pos[E] << ", XYZ=" << delta_xyz << ")";
m_width = DEFAULT_TOOLPATH_WIDTH;
}
if (m_width == 0.0f)
m_width = DEFAULT_TOOLPATH_WIDTH;
@@ -3348,7 +3660,6 @@ void GCodeProcessor::process_G2_G3(const GCodeReader::GCodeLine& line)
//BBS: axis reversal
std::max(-v_exit, v_entry));
float axis_max_jerk = get_axis_max_jerk(static_cast<PrintEstimatedStatistics::ETimeMode>(i), static_cast<Axis>(a));
if (jerk > axis_max_jerk) {
v_factor *= axis_max_jerk / jerk;
@@ -3580,6 +3891,19 @@ void GCodeProcessor::process_G92(const GCodeReader::GCodeLine& line)
m_origin[a] = m_end_position[a];
}
}
if (std::isnan(m_origin[X]) || std::isinf(m_origin[X]) ||
std::isnan(m_origin[Y]) || std::isinf(m_origin[Y]) ||
std::isnan(m_origin[Z]) || std::isinf(m_origin[Z])) {
BOOST_LOG_TRIVIAL(error) << "SM Orca: Invalid m_origin after G92 processing"
<< " extruder=" << static_cast<int>(m_extruder_id)
<< " m_origin=(" << m_origin[X] << ", " << m_origin[Y] << ", " << m_origin[Z] << ")"
<< " m_end_position=(" << m_end_position[X] << ", " << m_end_position[Y] << ", " << m_end_position[Z] << ")";
// 重置为0,防止污染继续传播
m_origin[X] = std::isnan(m_origin[X]) || std::isinf(m_origin[X]) ? 0.0f : m_origin[X];
m_origin[Y] = std::isnan(m_origin[Y]) || std::isinf(m_origin[Y]) ? 0.0f : m_origin[Y];
m_origin[Z] = std::isnan(m_origin[Z]) || std::isinf(m_origin[Z]) ? 0.0f : m_origin[Z];
}
}
void GCodeProcessor::process_M1(const GCodeReader::GCodeLine& line)
@@ -3709,7 +4033,6 @@ void GCodeProcessor::process_M191(const GCodeReader::GCodeLine& line)
simulate_st_synchronize(wait_chamber_temp_time);
}
void GCodeProcessor::process_M201(const GCodeReader::GCodeLine& line)
{
// see http://reprap.org/wiki/G-code#M201:_Set_max_printing_acceleration
@@ -4045,17 +4368,54 @@ void GCodeProcessor::run_post_process()
double filament_total_cost = 0.0;
for (const auto& [id, volume] : m_result.print_statistics.total_volumes_per_extruder) {
filament_mm[id] = volume / (static_cast<double>(M_PI) * sqr(0.5 * m_result.filament_diameters[id]));
if (id >= m_result.filament_diameters.size() ||
id >= m_result.filament_densities.size() ||
id >= m_result.filament_costs.size()) {
BOOST_LOG_TRIVIAL(error) << "SM Orca: Filament index " << id << " out of bounds (sizes: "
<< "diameter=" << m_result.filament_diameters.size()
<< ", density=" << m_result.filament_densities.size()
<< ", cost=" << m_result.filament_costs.size() << "), skipping cost calculation";
continue;
}
double diameter = m_result.filament_diameters[id];
double density = m_result.filament_densities[id];
double cost = m_result.filament_costs[id];
if (diameter <= 0.0 || std::isnan(diameter)) {
BOOST_LOG_TRIVIAL(warning) << "SM Orca: Invalid filament diameter " << diameter
<< " for filament " << id << ", using default 1.75mm";
diameter = 1.75;
}
if (density <= 0.0 || std::isnan(density)) {
BOOST_LOG_TRIVIAL(warning) << "SM Orca: Invalid filament density " << density
<< " for filament " << id << ", using default 1.25 g/cm³";
density = 1.25;
}
if (cost < 0.0 || std::isnan(cost)) {
BOOST_LOG_TRIVIAL(warning) << "SM Orca: Invalid filament cost " << cost
<< " for filament " << id << ", using 0.0";
cost = 0.0;
}
double cross_section = M_PI * sqr(0.5 * diameter);
filament_mm[id] = volume / cross_section;
filament_cm3[id] = volume * 0.001;
filament_g[id] = filament_cm3[id] * double(m_result.filament_densities[id]);
filament_cost[id] = filament_g[id] * double(m_result.filament_costs[id]) * 0.001;
filament_g[id] = filament_cm3[id] * density;
filament_cost[id] = filament_g[id] * cost * 0.001;
filament_total_g += filament_g[id];
filament_total_cost += filament_cost[id];
BOOST_LOG_TRIVIAL(debug) << "SM Orca: Filament " << id
<< " - volume: " << volume << "mm³, length: " << filament_mm[id]
<< "mm, weight: " << filament_g[id] << "g, cost: " << filament_cost[id];
}
double total_g_wipe_tower = m_print->print_statistics().total_wipe_tower_filament;
auto time_in_minutes = [](float time_in_seconds) {
assert(time_in_seconds >= 0.f);
return int((time_in_seconds + 0.5f) / 60.0f);
@@ -4183,7 +4543,6 @@ void GCodeProcessor::run_post_process()
size_t m_times_cache_id{ 0 };
size_t m_out_file_pos{ 0 };
public:
ExportLines(EWriteType type,
const std::array<TimeMachine, static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Count)>& machines)
@@ -4753,7 +5112,6 @@ void GCodeProcessor::run_post_process()
export_lines.flush(out, m_result, out_path);
out.close();
in.close();
@@ -4771,6 +5129,10 @@ void GCodeProcessor::store_move_vertex(EMoveType type, EMovePathType path_type)
m_line_id + 1 :
((type == EMoveType::Seam) ? m_last_line_id : m_line_id);
Vec3f extruder_offset = (static_cast<size_t>(m_extruder_id) < m_extruder_offsets.size())
? m_extruder_offsets[m_extruder_id]
: Vec3f(0.0f, 0.0f, 0.0f);
//BBS: apply plate's and extruder's offset to arc interpolation points
if (path_type == EMovePathType::Arc_move_cw ||
path_type == EMovePathType::Arc_move_ccw) {
@@ -4779,7 +5141,38 @@ void GCodeProcessor::store_move_vertex(EMoveType type, EMovePathType path_type)
Vec3f(m_interpolation_points[i].x() + m_x_offset,
m_interpolation_points[i].y() + m_y_offset,
m_processing_start_custom_gcode ? m_first_layer_height : m_interpolation_points[i].z()) +
m_extruder_offsets[m_extruder_id];
extruder_offset;
}
if (std::isnan(m_width) || std::isinf(m_width)) {
BOOST_LOG_TRIVIAL(error) << "SM Orca: Blocking invalid width: " << m_width
<< " (extruder " << m_extruder_id << ")";
m_width = DEFAULT_TOOLPATH_WIDTH;
}
if (std::isnan(m_height) || std::isinf(m_height)) {
BOOST_LOG_TRIVIAL(error) << "SM Orca: Blocking invalid height: " << m_height
<< " (extruder " << m_extruder_id << ")";
m_height = DEFAULT_TOOLPATH_HEIGHT;
}
Vec3f final_position = Vec3f(m_end_position[X] + m_x_offset,
m_end_position[Y] + m_y_offset,
m_processing_start_custom_gcode ? m_first_layer_height : m_end_position[Z] - m_z_offset)
+ extruder_offset;
if (std::isnan(final_position.x()) || std::isinf(final_position.x()) ||
std::isnan(final_position.y()) || std::isinf(final_position.y()) ||
std::isnan(final_position.z()) || std::isinf(final_position.z())) {
BOOST_LOG_TRIVIAL(error) << "SM Orca: Invalid position calculated for extruder " << static_cast<int>(m_extruder_id)
<< " position=(" << final_position.x() << ", " << final_position.y() << ", " << final_position.z() << ")"
<< " m_end_position=(" << m_end_position[X] << ", " << m_end_position[Y] << ", " << m_end_position[Z] << ")"
<< " offset=(" << m_x_offset << ", " << m_y_offset << ", " << m_z_offset << ")"
<< " extruder_offset=(" << extruder_offset.x() << ", " << extruder_offset.y() << ", " << extruder_offset.z() << ")";
// 使用不带offset的position作为fallback
final_position = Vec3f(m_end_position[X] + m_x_offset,
m_end_position[Y] + m_y_offset,
m_processing_start_custom_gcode ? m_first_layer_height : m_end_position[Z] - m_z_offset);
}
m_result.moves.push_back({
@@ -4789,7 +5182,7 @@ void GCodeProcessor::store_move_vertex(EMoveType type, EMovePathType path_type)
m_extruder_id,
m_cp_color.current,
//BBS: add plate's offset to the rendering vertices
Vec3f(m_end_position[X] + m_x_offset, m_end_position[Y] + m_y_offset, m_processing_start_custom_gcode ? m_first_layer_height : m_end_position[Z]- m_z_offset) + m_extruder_offsets[m_extruder_id],
final_position,
static_cast<float>(m_end_position[E] - m_start_position[E]),
m_feedrate,
m_width,
+13
View File
@@ -14,6 +14,7 @@
#include <string>
#include <string_view>
#include <optional>
#include <unordered_map>
namespace Slic3r {
@@ -677,6 +678,8 @@ class Print;
EPositioningType m_global_positioning_type;
EPositioningType m_e_local_positioning_type;
std::vector<Vec3f> m_extruder_offsets;
// SM Orca: 耗材到物理挤出机的映射
std::unordered_map<int, int> m_filament_extruder_map;
GCodeFlavor m_flavor;
float m_nozzle_volume;
AxisCoords m_start_position; // mm
@@ -776,6 +779,16 @@ class Print;
void apply_config(const PrintConfig& config);
void set_print(Print* print) { m_print = print; }
// SM Orca: 设置耗材到物理挤出机的映射
void set_filament_extruder_map(const std::unordered_map<int, int>& map) {
m_filament_extruder_map = map;
}
// SM Orca: 获取物理挤出机ID(根据耗材索引)
int get_physical_extruder(int filament_idx) const {
auto it = m_filament_extruder_map.find(filament_idx);
int physical_extruder_id = (it != m_filament_extruder_map.end()) ? it->second : filament_idx;
return physical_extruder_id;
}
void enable_stealth_time_estimator(bool enabled);
bool is_stealth_time_estimator_enabled() const {
return m_time_processor.machines[static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Stealth)].enabled;
+16 -6
View File
@@ -668,18 +668,20 @@ WipeTower::WipeTower(const PrintConfig& config, int plate_idx, Vec3d plate_origi
void WipeTower::set_extruder(size_t idx, const PrintConfig& config)
void WipeTower::set_extruder(size_t idx, int physical_extruder, const PrintConfig& config)
{
//while (m_filpar.size() < idx+1) // makes sure the required element is in the vector
m_filpar.push_back(FilamentParameters());
// SM Orca: 耗材属性使用 idx (filament index)
m_filpar[idx].material = config.filament_type.get_at(idx);
// m_filpar[idx].is_soluble = config.filament_soluble.get_at(idx);
m_filpar[idx].is_soluble = config.wipe_tower_filament == 0 ? config.filament_soluble.get_at(idx) : (idx != size_t(config.wipe_tower_filament - 1));
// BBS
m_filpar[idx].is_support = config.filament_is_support.get_at(idx);
m_filpar[idx].nozzle_temperature = config.nozzle_temperature.get_at(idx);
m_filpar[idx].nozzle_temperature_initial_layer = config.nozzle_temperature_initial_layer.get_at(idx);
// SM Orca: 温度是挤出机属性,使用 physical_extruder
m_filpar[idx].nozzle_temperature = config.nozzle_temperature.get_at(physical_extruder);
m_filpar[idx].nozzle_temperature_initial_layer = config.nozzle_temperature_initial_layer.get_at(physical_extruder);
// If this is a single extruder MM printer, we will use all the SE-specific config values.
// Otherwise, the defaults will be used to turn off the SE stuff.
@@ -698,14 +700,19 @@ void WipeTower::set_extruder(size_t idx, const PrintConfig& config)
#endif
m_filpar[idx].filament_area = float((M_PI/4.f) * pow(config.filament_diameter.get_at(idx), 2)); // all extruders are assumed to have the same filament diameter at this point
float nozzle_diameter = float(config.nozzle_diameter.get_at(idx));
// SM Orca: 喷嘴直径是挤出机属性,使用 physical_extruder
float nozzle_diameter = float(config.nozzle_diameter.get_at(physical_extruder));
m_filpar[idx].nozzle_diameter = nozzle_diameter; // to be used in future with (non-single) multiextruder MM
float max_vol_speed = float(config.filament_max_volumetric_speed.get_at(idx));
if (max_vol_speed!= 0.f)
m_filpar[idx].max_e_speed = (max_vol_speed / filament_area());
m_perimeter_width = nozzle_diameter * Width_To_Nozzle_Ratio; // all extruders are now assumed to have the same diameter
// SM Orca: Store per-filament perimeter width and also set the global one
// Note: m_perimeter_width gets overwritten with each set_extruder() call
// The brim should use m_filpar[0].perimeter_width for consistency
m_filpar[idx].perimeter_width = nozzle_diameter * Width_To_Nozzle_Ratio;
m_perimeter_width = m_filpar[idx].perimeter_width; // all extruders are now assumed to have the same diameter
// BBS: remove useless config
#if 0
if (m_semm) {
@@ -1305,7 +1312,10 @@ WipeTower::ToolChangeResult WipeTower::finish_layer(bool extrude_perimeter, bool
}
// brim chamfer
float spacing = m_perimeter_width - m_layer_height * float(1. - M_PI_4);
// SM Orca: Use first filament's perimeter width for consistent brim spacing
// The brim is generated once for the entire wipe tower and should use a consistent spacing
float brim_perimeter_width = m_filpar.empty() ? m_perimeter_width : m_filpar[0].perimeter_width;
float spacing = brim_perimeter_width - m_layer_height * float(1. - M_PI_4);
// How many perimeters shall the brim have?
int loops_num = (m_wipe_tower_brim_width + spacing / 2.f) / spacing;
const float max_chamfer_width = 3.f;
+4 -1
View File
@@ -144,7 +144,8 @@ public:
// Set the extruder properties.
void set_extruder(size_t idx, const PrintConfig& config);
// SM Orca: 添加 physical_extruder 参数,用于支持耗材-挤出机映射
void set_extruder(size_t idx, int physical_extruder, const PrintConfig& config);
// Appends into internal structure m_plan containing info about the future wipe tower
// to be used before building begins. The entries must be added ordered in z.
@@ -269,6 +270,8 @@ public:
std::vector<float> ramming_speed;
float nozzle_diameter;
float filament_area;
// SM Orca: Store per-filament perimeter width for correct brim generation
float perimeter_width = 0.f;
};
private:
File diff suppressed because it is too large Load Diff
+5 -1
View File
@@ -45,7 +45,9 @@ public:
// Set the extruder properties.
void set_extruder(size_t idx, const PrintConfig& config);
// SM Orca: 添加 physical_extruder 参数,用于支持耗材-挤出机映射
// idx: 耗材索引, physical_extruder: 物理挤出机索引
void set_extruder(size_t idx, int physical_extruder, const PrintConfig& config);
// Appends into internal structure m_plan containing info about the future wipe tower
// to be used before building begins. The entries must be added ordered in z.
@@ -160,6 +162,8 @@ public:
float filament_minimal_purge_on_wipe_tower = 0.f;
float retract_length;
float retract_speed;
// SM Orca: Store per-filament perimeter width for correct brim generation
float perimeter_width = 0.f;
};
private:
+14 -6
View File
@@ -27,6 +27,10 @@ void GCodeWriter::apply_print_config(const PrintConfig &print_config)
{
this->config.apply(print_config, true);
m_single_extruder_multi_material = print_config.single_extruder_multi_material.value;
m_physical_extruder_count = print_config.nozzle_diameter.values.size();
if (m_physical_extruder_count == 0) {
m_physical_extruder_count = 1; // 防止除零,默认为1
}
bool use_mach_limits = print_config.gcode_flavor.value == gcfMarlinLegacy || print_config.gcode_flavor.value == gcfMarlinFirmware ||
print_config.gcode_flavor.value == gcfKlipper || print_config.gcode_flavor.value == gcfRepRapFirmware;
m_max_acceleration = std::lrint(use_mach_limits ? print_config.machine_max_acceleration_extruding.values.front() : 0);
@@ -45,16 +49,17 @@ void GCodeWriter::apply_print_config(const PrintConfig &print_config)
void GCodeWriter::set_extruders(std::vector<unsigned int> extruder_ids)
{
std::sort(extruder_ids.begin(), extruder_ids.end());
m_extruder = nullptr; // this points to object inside `m_extruders`, so should be cleared too
m_extruders.clear();
m_extruders.reserve(extruder_ids.size());
for (unsigned int extruder_id : extruder_ids)
m_extruders.emplace_back(Extruder(extruder_id, &this->config, config.single_extruder_multi_material.value));
for (unsigned int extruder_id : extruder_ids) {
int physical_extruder_id = get_physical_extruder(extruder_id);
m_extruders.emplace_back(Extruder(extruder_id, physical_extruder_id, &this->config, config.single_extruder_multi_material.value));
}
/* we enable support for multiple extruder if any extruder greater than 0 is used
(even if prints only uses that one) since we need to output Tx commands
first extruder has index 0 */
this->multiple_extruders = (*std::max_element(extruder_ids.begin(), extruder_ids.end())) > 0;
}
@@ -399,7 +404,6 @@ std::string GCodeWriter::set_input_shaping(char axis, float damp, float freq) co
return gcode.str();
}
std::string GCodeWriter::reset_e(bool force)
{
if (FLAVOR_IS(gcfMach3)
@@ -454,6 +458,9 @@ std::string GCodeWriter::toolchange_prefix() const
std::string GCodeWriter::toolchange(unsigned int extruder_id)
{
int physical_extruder = get_physical_extruder(extruder_id);
// set the new extruder
auto it_extruder = Slic3r::lower_bound_by_predicate(m_extruders.begin(), m_extruders.end(), [extruder_id](const Extruder &e) { return e.id() < extruder_id; });
assert(it_extruder != m_extruders.end() && it_extruder->id() == extruder_id);
@@ -469,6 +476,7 @@ std::string GCodeWriter::toolchange(unsigned int extruder_id)
gcode << " ; change extruder";
gcode << "\n";
gcode << this->reset_e(true);
} else {
}
return gcode.str();
}
+26
View File
@@ -4,6 +4,7 @@
#include "libslic3r.h"
#include <string>
#include <charconv>
#include <unordered_map>
#include "Extruder.hpp"
#include "Point.hpp"
#include "PrintConfig.hpp"
@@ -119,6 +120,26 @@ public:
void set_is_first_layer(bool bval) { m_is_first_layer = bval; }
GCodeFlavor get_gcode_flavor() const { return config.gcode_flavor; }
// SM Orca: 设置耗材-挤出机映射
void set_filament_extruder_map(const std::unordered_map<int, int>& map) { m_filament_extruder_map = map; }
const std::unordered_map<int, int>& get_filament_extruder_map() const { return m_filament_extruder_map; }
// SM Orca: 获取物理挤出机ID
// 关键修复:当映射表为空时,使用模运算而不是直接返回耗材ID,避免越界
// 例如:4个物理挤出机时,耗材0-7分别映射到0,1,2,3,0,1,2,3
int get_physical_extruder(int filament_idx) const {
auto it = m_filament_extruder_map.find(filament_idx);
int physical_extruder_id;
if (it != m_filament_extruder_map.end()) {
// 从映射表获取
physical_extruder_id = it->second;
} else {
// 映射表为空或没有该耗材的映射,使用默认模运算映射
physical_extruder_id = filament_idx % m_physical_extruder_count;
}
return physical_extruder_id;
}
// Returns whether this flavor supports separate print and travel acceleration.
static bool supports_separate_travel_acceleration(GCodeFlavor flavor);
private:
@@ -170,6 +191,11 @@ public:
double m_current_speed;
bool m_is_first_layer = true;
// SM Orca: 耗材到物理挤出机的映射表(filament_idx -> physical_extruder_id
std::unordered_map<int, int> m_filament_extruder_map;
// SM Orca: 物理挤出机数量(用于默认模运算映射)
size_t m_physical_extruder_count = 1; // 默认为1,防止除零
enum class Acceleration {
Travel,
Print
+115 -28
View File
@@ -293,8 +293,6 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
|| opt_key == "wipe_tower_no_sparse_layers"
|| opt_key == "flush_volumes_matrix"
|| opt_key == "prime_volume"
|| opt_key == "prime_tower_brim_chamfer"
|| opt_key == "prime_tower_brim_chamfer_max_width"
|| opt_key == "flush_into_infill"
|| opt_key == "flush_into_support"
|| opt_key == "initial_layer_infill_speed"
@@ -495,6 +493,56 @@ std::vector<unsigned int> Print::extruders(bool conside_custom_gcode) const
return extruders;
}
// This must be called before the mapping is used (e.g., before export_gcode)
void Print::initialize_filament_extruder_map()
{
m_filament_extruder_map.clear();
// Get the number of physical extruders (number of nozzle_diameter entries)
size_t physical_extruder_count = m_config.nozzle_diameter.values.size();
if (physical_extruder_count == 0) {
BOOST_LOG_TRIVIAL(error) << "Print::initialize_filament_extruder_map: ERROR - No physical extruders configured!";
return;
}
// Get all filament indices that will be used
std::vector<unsigned int> filament_extruders = this->extruders();
// IMPORTANT: Always create mappings for ALL configured filaments, not just those used by objects.
// This is critical because filament override parameters need to access the mapping for all filaments.
// For example, if a user has 8 filaments configured but only uses 4 in their model,
// the mapping table must still contain entries for all 8 filaments to correctly
// inherit parameters from the corresponding physical extruders.
// extruders() returns empty. In this case, use filament_diameter.size() to determine filament count.
// This ensures the mapping is created for all configured filaments, not just those used by objects.
if (filament_extruders.empty()) {
size_t filament_count = m_config.filament_diameter.size();
for (size_t i = 0; i < filament_count; ++i) {
filament_extruders.push_back((unsigned int)i);
}
} else {
// Even if extruders() returns some values, we need to ensure ALL configured filaments are in the map.
// Add any missing filament indices that are configured but not used by objects.
size_t configured_filament_count = m_config.filament_diameter.size();
for (size_t i = 0; i < configured_filament_count; ++i) {
if (std::find(filament_extruders.begin(), filament_extruders.end(), (unsigned int)i) == filament_extruders.end()) {
filament_extruders.push_back((unsigned int)i);
}
}
}
// Create mapping: filament_id -> physical_extruder_id
// Mapping formula: physical_extruder = filament_id % physical_extruder_count
// This allows using 8 filaments with 4 physical extruders:
// filament 0,1,2,3 -> extruder 0,1,2,3
// filament 4,5,6,7 -> extruder 0,1,2,3
for (unsigned int filament_idx : filament_extruders) {
int physical_extruder = filament_idx % physical_extruder_count;
m_filament_extruder_map[filament_idx] = physical_extruder;
}
}
unsigned int Print::num_object_instances() const
{
unsigned int instances = 0;
@@ -506,8 +554,10 @@ unsigned int Print::num_object_instances() const
double Print::max_allowed_layer_height() const
{
double nozzle_diameter_max = 0.;
for (unsigned int extruder_id : this->extruders())
nozzle_diameter_max = std::max(nozzle_diameter_max, m_config.nozzle_diameter.get_at(extruder_id));
for (unsigned int extruder_id : this->extruders()) {
int physical_extruder = get_physical_extruder(extruder_id);
nozzle_diameter_max = std::max(nozzle_diameter_max, m_config.nozzle_diameter.get_at(physical_extruder));
}
return nozzle_diameter_max;
}
@@ -1185,10 +1235,12 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons*
if (this->has_wipe_tower() && ! m_objects.empty()) {
// Make sure all extruders use same diameter filament and have the same nozzle diameter
// EPSILON comparison is used for nozzles and 10 % tolerance is used for filaments
double first_nozzle_diam = m_config.nozzle_diameter.get_at(extruders.front());
int first_physical = get_physical_extruder(extruders.front());
double first_nozzle_diam = m_config.nozzle_diameter.get_at(first_physical);
double first_filament_diam = m_config.filament_diameter.get_at(extruders.front());
for (const auto& extruder_idx : extruders) {
double nozzle_diam = m_config.nozzle_diameter.get_at(extruder_idx);
int physical_extruder = get_physical_extruder(extruder_idx);
double nozzle_diam = m_config.nozzle_diameter.get_at(physical_extruder);
double filament_diam = m_config.filament_diameter.get_at(extruder_idx);
if (nozzle_diam - EPSILON > first_nozzle_diam || nozzle_diam + EPSILON < first_nozzle_diam
|| std::abs((filament_diam - first_filament_diam) / first_filament_diam) > 0.1) {
@@ -1294,7 +1346,8 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons*
double min_nozzle_diameter = std::numeric_limits<double>::max();
double max_nozzle_diameter = 0;
for (unsigned int extruder_id : extruders) {
double dmr = m_config.nozzle_diameter.get_at(extruder_id);
int physical_extruder = get_physical_extruder(extruder_id);
double dmr = m_config.nozzle_diameter.get_at(physical_extruder);
min_nozzle_diameter = std::min(min_nozzle_diameter, dmr);
max_nozzle_diameter = std::max(max_nozzle_diameter, dmr);
}
@@ -1382,9 +1435,10 @@ StringObjectException Print::validate(StringObjectException *warning, Polygons*
size_t first_layer_extruder = object->config().raft_layers == 1
? object->config().support_interface_filament-1
: object->config().support_filament-1;
int physical_extruder = get_physical_extruder(first_layer_extruder);
first_layer_min_nozzle_diameter = (first_layer_extruder == size_t(-1)) ?
min_nozzle_diameter :
m_config.nozzle_diameter.get_at(first_layer_extruder);
m_config.nozzle_diameter.get_at(physical_extruder);
} else {
// if we don't have raft layers, any nozzle diameter is potentially used in first layer
first_layer_min_nozzle_diameter = min_nozzle_diameter;
@@ -1702,11 +1756,13 @@ Flow Print::brim_flow() const
extruders and take the one with, say, the smallest index.
The same logic should be applied to the code that selects the extruder during G-code
generation as well. */
int filament_idx = m_print_regions.front()->config().wall_filament - 1;
int physical_extruder = get_physical_extruder(filament_idx);
return Flow::new_from_config_width(
frPerimeter,
// Flow::new_from_config_width takes care of the percent to value substitution
width,
(float)m_config.nozzle_diameter.get_at(m_print_regions.front()->config().wall_filament-1),
(float)m_config.nozzle_diameter.get_at(physical_extruder),
(float)this->skirt_first_layer_height());
}
@@ -1721,11 +1777,13 @@ Flow Print::skirt_flow() const
extruders and take the one with, say, the smallest index;
The same logic should be applied to the code that selects the extruder during G-code
generation as well. */
int filament_idx = m_objects.front()->config().support_filament - 1;
int physical_extruder = get_physical_extruder(filament_idx);
return Flow::new_from_config_width(
frPerimeter,
// Flow::new_from_config_width takes care of the percent to value substitution
width,
(float)m_config.nozzle_diameter.get_at(m_objects.front()->config().support_filament-1),
(float)m_config.nozzle_diameter.get_at(physical_extruder),
(float)this->skirt_first_layer_height());
}
@@ -1804,7 +1862,6 @@ void PrintObject::copy_layers_overhang_from_shared_object()
}
}
// BBS
BoundingBox PrintObject::get_first_layer_bbox(float& a, float& layer_height, std::string& name)
{
@@ -2155,7 +2212,6 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
append(m_first_layer_convex_hull.points, std::move(poly.points));
}
if (has_skirt() && ! draft_shield) {
// In case that draft shield is NOT active, generate skirt now.
// It will be placed around the brim, so brim has to be ready.
@@ -2243,11 +2299,42 @@ std::string Print::export_gcode(const std::string& path_template, GCodeProcessor
//BBS: compute plate offset for gcode-generator
const Vec3d origin = this->get_plate_origin();
gcode.set_gcode_offset(origin(0), origin(1));
this->initialize_filament_extruder_map();
for (const auto& pair : m_filament_extruder_map) {
}
gcode.set_filament_extruder_map(m_filament_extruder_map);
gcode.do_export(this, path.c_str(), result, thumbnail_cb);
//BBS
result->conflict_result = m_conflict_result;
return path.c_str();
// After G-code export is complete, finalize the output path by replacing placeholders with actual values
// This ensures that placeholders like {print_time} are replaced with calculated values
// Note: This is needed for direct export (not through BackgroundSlicingProcess) where
// finalize_output_path() might not be called automatically
std::string final_path = this->print_statistics().finalize_output_path(path);
// Rename the file from the placeholder path to the finalized path
if (final_path != path) {
std::error_code ret = rename_file(path, final_path);
if (ret) {
BOOST_LOG_TRIVIAL(warning) << "Failed to rename G-code file from '" << path
<< "' to '" << final_path << "': " << ret.message();
// If rename fails, return the original path
return path;
} else {
BOOST_LOG_TRIVIAL(info) << "Renamed G-code file from '" << path
<< "' to '" << final_path << "'";
// Update result filename to reflect the new path
if (result) {
result->filename = final_path;
}
}
}
return final_path;
}
void Print::_make_skirt()
@@ -2333,7 +2420,8 @@ void Print::_make_skirt()
extruders_e_per_mm.reserve(set_extruders.size());
for (auto &extruder_id : set_extruders) {
extruders.push_back(extruder_id);
extruders_e_per_mm.push_back(Extruder((unsigned int)extruder_id, &m_config, m_config.single_extruder_multi_material).e_per_mm(mm3_per_mm));
int physical_extruder_id = get_physical_extruder(extruder_id);
extruders_e_per_mm.push_back(Extruder((unsigned int)extruder_id, physical_extruder_id, &m_config, m_config.single_extruder_multi_material).e_per_mm(mm3_per_mm));
}
}
@@ -2733,8 +2821,10 @@ void Print::_make_wipe_tower()
// wipe_tower.set_zhop();
// Set the extruder & material properties at the wipe tower object.
for (size_t i = 0; i < number_of_extruders; ++i)
wipe_tower.set_extruder(i, m_config);
for (size_t i = 0; i < number_of_extruders; ++i) {
int physical_extruder = get_physical_extruder(i);
wipe_tower.set_extruder(i, physical_extruder, m_config);
}
// BBS: remove priming logic
// m_wipe_tower_data.priming = Slic3r::make_unique<std::vector<WipeTower::ToolChangeResult>>(
@@ -2829,8 +2919,10 @@ void Print::_make_wipe_tower()
// wipe_tower.set_zhop();
// Set the extruder & material properties at the wipe tower object.
for (size_t i = 0; i < number_of_extruders; ++i)
wipe_tower.set_extruder(i, m_config);
for (size_t i = 0; i < number_of_extruders; ++i) {
int physical_extruder = get_physical_extruder(i);
wipe_tower.set_extruder(i, physical_extruder, m_config);
}
m_wipe_tower_data.priming = Slic3r::make_unique<std::vector<WipeTower::ToolChangeResult>>(
wipe_tower.prime((float)this->skirt_first_layer_height(), m_wipe_tower_data.tool_ordering.all_extruders(), false));
@@ -2920,7 +3012,10 @@ std::string Print::output_filename(const std::string &filename_base) const
{
// Set the placeholders for the data know first after the G-code export is finished.
// These values will be just propagated into the output file name.
DynamicConfig config = this->finished() ? this->print_statistics().config() : this->print_statistics().placeholders();
// Use cached statistics if available (even if not finished) to avoid placeholders like {print_time}
const PrintStatistics& stats = this->print_statistics();
bool has_valid_stats = stats.total_used_filament > 0 || !stats.estimated_normal_print_time.empty();
DynamicConfig config = (this->finished() || has_valid_stats) ? stats.config() : stats.placeholders();
config.set_key_value("num_filaments", new ConfigOptionInt((int)m_config.nozzle_diameter.size()));
config.set_key_value("num_extruders", new ConfigOptionInt((int) m_config.nozzle_diameter.size()));
config.set_key_value("plate_name", new ConfigOptionString(get_plate_name()));
@@ -2970,6 +3065,7 @@ void Print::export_gcode_from_previous_file(const std::string& file, GCodeProces
GCodeProcessor::s_IsBBLPrinter = is_BBL_printer();
const Vec3d origin = this->get_plate_origin();
processor.set_xy_offset(origin(0), origin(1));
processor.set_filament_extruder_map(m_filament_extruder_map);
//processor.enable_producers(true);
processor.process_file(file);
@@ -3113,7 +3209,6 @@ const std::string PrintStatistics::TotalFilamentCostValueMask = "; total filamen
const std::string PrintStatistics::TotalFilamentUsedWipeTower = "total filament used for wipe tower [g]";
const std::string PrintStatistics::TotalFilamentUsedWipeTowerValueMask = "; total filament used for wipe tower [g] = %.2lf\n";
/*add json export/import related functions */
#define JSON_POLYGON_CONTOUR "contour"
#define JSON_POLYGON_HOLES "holes"
@@ -3123,7 +3218,6 @@ const std::string PrintStatistics::TotalFilamentUsedWipeTowerValueMask = "; tota
#define JSON_OBJECT_NAME "name"
#define JSON_IDENTIFY_ID "identify_id"
#define JSON_LAYERS "layers"
#define JSON_SUPPORT_LAYERS "support_layers"
#define JSON_TREE_SUPPORT_LAYERS "tree_support_layers"
@@ -3160,8 +3254,6 @@ const std::string PrintStatistics::TotalFilamentUsedWipeTowerValueMask = "; tota
#define JSON_LAYER_REGION_PERIMETERS "perimeters"
#define JSON_LAYER_REGION_FILLS "fills"
#define JSON_SURF_TYPE "surface_type"
#define JSON_SURF_THICKNESS "thickness"
#define JSON_SURF_THICKNESS_LAYER "thickness_layers"
@@ -3201,7 +3293,6 @@ const std::string PrintStatistics::TotalFilamentUsedWipeTowerValueMask = "; tota
#define JSON_EXTRUSION_NO_EXTRUSION "no_extrusion"
#define JSON_EXTRUSION_LOOP_ROLE "loop_role"
static void to_json(json& j, const Points& p_s) {
for (const Point& p : p_s)
{
@@ -3265,7 +3356,6 @@ static void to_json(json& j, const ArcSegment& arc_seg) {
j[JSON_ARC_CENTER] = std::move(center_point_json);
}
static void to_json(json& j, const Polyline& poly_line) {
json points_json = json::array(), fittings_json = json::array();
points_json = poly_line.points;
@@ -3539,7 +3629,6 @@ static void from_json(const json& j, ArcSegment& arc_seg) {
return;
}
static void from_json(const json& j, Polyline& poly_line) {
poly_line.points = j[JSON_POINTS];
@@ -3750,7 +3839,6 @@ static void convert_layer_region_from_json(const json& j, LayerRegion& layer_reg
return;
}
void extract_layer(const json& layer_json, Layer& layer) {
//slice_polygons
int slice_polygons_count = layer_json[JSON_LAYER_SLICED_POLYGONS].size();
@@ -4121,7 +4209,6 @@ int Print::export_cached_data(const std::string& directory, bool with_space)
return ret;
}
int Print::load_cached_data(const std::string& directory)
{
int ret = 0;
+32
View File
@@ -23,6 +23,7 @@
#include <functional>
#include <set>
#include <unordered_map>
#include "calib.hpp"
@@ -887,6 +888,34 @@ public:
std::vector<unsigned int> object_extruders() const;
std::vector<unsigned int> support_material_extruders() const;
// SM Orca: 设置耗材-挤出机映射
void set_filament_extruder_map(const std::unordered_map<int, int>& map) { m_filament_extruder_map = map; }
// SM Orca: 获取耗材-挤出机映射表
const std::unordered_map<int, int>& get_filament_extruder_map() const { return m_filament_extruder_map; }
// SM Orca: 获取物理挤出机ID(根据耗材索引)
// 关键修复:当映射表为空时,使用模运算而不是直接返回耗材ID,避免越界
int get_physical_extruder(int filament_idx) const {
auto it = m_filament_extruder_map.find(filament_idx);
int physical_extruder_id;
if (it != m_filament_extruder_map.end()) {
// 从映射表获取
physical_extruder_id = it->second;
} else {
// 映射表为空或没有该耗材的映射,使用默认模运算映射
size_t physical_count = m_config.nozzle_diameter.values.size();
if (physical_count == 0) {
// 防止除零,使用安全的默认值
physical_extruder_id = 0;
} else {
physical_extruder_id = filament_idx % physical_count;
}
}
return physical_extruder_id;
}
// SM Orca: Initialize filament-to-physical-extruder mapping table
void initialize_filament_extruder_map();
std::vector<unsigned int> extruders(bool conside_custom_gcode = false) const;
double max_allowed_layer_height() const;
bool has_support_material() const;
@@ -1066,6 +1095,9 @@ private:
//SoftFever: calibration
Calib_Params m_calib_params;
// SM Orca: 耗材到物理挤出机的映射表
std::unordered_map<int, int> m_filament_extruder_map;
// To allow GCode to set the Print's GCodeExport step status.
friend class GCode;
// Allow PrintObject to access m_mutex and m_cancel_callback.
+215 -6
View File
@@ -3,6 +3,7 @@
#include <boost/log/trivial.hpp>
#include <cfloat>
#include <sstream>
namespace Slic3r {
@@ -216,13 +217,133 @@ static bool custom_per_printz_gcodes_tool_changes_differ(const std::vector<Custo
return false;
}
// For each filament slot, if no override is provided, inherit from the mapped physical extruder
// IMPORTANT: This function creates a NEW target array with filament_count elements
static ConfigOption* apply_physical_extruder_defaults(
const ConfigOption* filament_overrides,
const ConfigOption* extruder_defaults,
size_t filament_count,
const std::unordered_map<int, int>& filament_extruder_map)
{
if (!extruder_defaults->is_vector())
return nullptr;
auto* extruder_vec = dynamic_cast<const ConfigOptionVectorBase*>(extruder_defaults);
const ConfigOptionVectorBase* override_vec = filament_overrides ?
dynamic_cast<const ConfigOptionVectorBase*>(filament_overrides) : nullptr;
if (!extruder_vec)
return nullptr;
// Clone the extruder defaults to create the target
auto* target = extruder_defaults->clone();
auto* target_vec = dynamic_cast<ConfigOptionVectorBase*>(target);
if (!target_vec) {
delete target;
return nullptr;
}
// Resize target to filament_count
target_vec->resize(filament_count);
for (size_t filament_idx = 0; filament_idx < filament_count; ++filament_idx) {
bool has_override = false;
if (override_vec && filament_idx < override_vec->size()) {
if (override_vec->nullable()) {
// Nullable type: use override only if not nil (checkbox is checked)
has_override = !override_vec->is_nil(filament_idx);
} else {
// Non-nullable type: check if the value differs from the default (printer config)
// Only if it's different do we consider it a user override
// Get the default value from the mapped physical extruder
auto map_it = filament_extruder_map.find(filament_idx);
int physical_extruder_idx;
if (map_it != filament_extruder_map.end()) {
physical_extruder_idx = map_it->second;
} else {
// Fallback: use modulo to map filament to physical extruder
// This handles edge cases where the map is incomplete or filament_idx is out of range
size_t physical_extruder_count = extruder_vec->size();
if (physical_extruder_count == 0) {
// Should not happen, but safety check
physical_extruder_idx = 0;
} else {
physical_extruder_idx = (int)filament_idx % (int)physical_extruder_count;
}
}
if (physical_extruder_idx < extruder_vec->size()) {
// Try different types: double, int, bool
auto* override_dbl = dynamic_cast<const ConfigOptionVector<double>*>(override_vec);
auto* extruder_dbl = dynamic_cast<const ConfigOptionVector<double>*>(extruder_vec);
if (override_dbl && extruder_dbl) {
// Compare with the value from the mapped physical extruder
double override_value = override_dbl->get_at(filament_idx);
double default_value = extruder_dbl->get_at(physical_extruder_idx);
// Use override only if value differs from default
has_override = (override_value != default_value);
} else {
// Try int type
auto* override_int = dynamic_cast<const ConfigOptionVector<int>*>(override_vec);
auto* extruder_int = dynamic_cast<const ConfigOptionVector<int>*>(extruder_vec);
if (override_int && extruder_int) {
int override_value = override_int->get_at(filament_idx);
int default_value = extruder_int->get_at(physical_extruder_idx);
has_override = (override_value != default_value);
} else {
// Try bool type
auto* override_bool = dynamic_cast<const ConfigOptionVector<unsigned char>*>(override_vec);
auto* extruder_bool = dynamic_cast<const ConfigOptionVector<unsigned char>*>(extruder_vec);
if (override_bool && extruder_bool) {
unsigned char override_value = override_bool->get_at(filament_idx);
unsigned char default_value = extruder_bool->get_at(physical_extruder_idx);
has_override = (override_value != default_value);
}
}
}
}
}
}
if (!has_override) {
// No override: inherit from the mapped physical extruder
auto map_it = filament_extruder_map.find(filament_idx);
int physical_extruder_idx;
if (map_it != filament_extruder_map.end()) {
physical_extruder_idx = map_it->second;
} else {
// Fallback: use modulo to map filament to physical extruder
// This handles edge cases where the map is incomplete or filament_idx is out of range
size_t physical_extruder_count = extruder_vec->size();
if (physical_extruder_count == 0) {
// Should not happen, but safety check
physical_extruder_idx = 0;
} else {
physical_extruder_idx = (int)filament_idx % (int)physical_extruder_count;
}
}
if (physical_extruder_idx < extruder_vec->size() && filament_idx < target_vec->size()) {
target_vec->set_at(extruder_vec, filament_idx, physical_extruder_idx);
}
} else if (override_vec && filament_idx < override_vec->size()) {
// Has override: use the value from filament config
target_vec->set_at(override_vec, filament_idx, filament_idx);
}
}
return target;
}
// Collect changes to print config, account for overrides of extruder retract values by filament presets.
//BBS: add plate index
static t_config_option_keys print_config_diffs(
const PrintConfig &current_config,
const DynamicPrintConfig &new_full_config,
DynamicPrintConfig &filament_overrides,
int plate_index)
int plate_index,
const std::unordered_map<int, int> &filament_extruder_map)
{
const std::vector<std::string> &extruder_retract_keys = print_config_def.extruder_retract_keys();
const std::string filament_prefix = "filament_";
@@ -241,25 +362,81 @@ static t_config_option_keys print_config_diffs(
// const ConfigOption *opt_new_filament = std::binary_search(extruder_retract_keys.begin(), extruder_retract_keys.end(), opt_key) ? new_full_config.option(filament_prefix + opt_key) : nullptr;
const ConfigOption* opt_new_filament = (iter == extruder_retract_keys.end()) ? nullptr :
new_full_config.option(filament_prefix + opt_key);
if (opt_new_filament != nullptr && ! opt_new_filament->is_nil()) {
bool is_extruder_retract_param = (iter != extruder_retract_keys.end());
// 1. This is an extruder retract parameter AND
// 2. Filament overrides exist AND
// 3. Filament-extruder map is not empty (meaning objects are loaded and mapping is initialized)
// When user edits printer config directly (without objects loaded or without filament overrides),
// we should treat it as a regular config change to ensure UI updates work correctly.
bool has_filament_overrides = (opt_new_filament != nullptr && !opt_new_filament->is_nil());
bool needs_physical_mapping = is_extruder_retract_param && has_filament_overrides && !filament_extruder_map.empty();
if (needs_physical_mapping) {
// This is safe because both opt_old and opt_new should have the same number of physical extruders
bool printer_config_changed = (*opt_old != *opt_new);
auto* override_vec = dynamic_cast<const ConfigOptionVectorBase*>(opt_new_filament);
if (override_vec) {
BOOST_LOG_TRIVIAL(info) << "print_config_diffs: " << opt_key
<< " - filament_override size=" << override_vec->size()
<< ", nullable=" << override_vec->nullable();
}
// since we know has_filament_overrides is true at this point
if ((opt_key == "long_retractions_when_cut" || opt_key == "retraction_distances_when_cut")
&& new_full_config.option<ConfigOptionInt>("enable_long_retraction_when_cut")->value != LongRectrationLevel::EnableFilament)
continue;
// - Check if printer config or filament override changed the effective value
// - Only add to print_diff (not filament_overrides) to avoid array size mismatch
// The actual filament->extruder mapping is applied later during config usage
//
// 关键修复:只要打印机配置变化了,就应该添加到 print_diff
// 这确保了用户在UI中修改打印机配置时,修改能被正确保存
auto opt_copy = opt_new->clone();
opt_copy->apply_override(opt_new_filament);
if (printer_config_changed || *opt_old != *opt_copy) {
print_diff.emplace_back(opt_key);
BOOST_LOG_TRIVIAL(info) << "print_config_diffs: " << opt_key
<< " - adding to print_diff (printer_changed=" << (printer_config_changed ? "Y" : "N")
<< ", effective_changed=" << (*opt_old != *opt_copy ? "Y" : "N") << ")";
}
delete opt_copy;
} else if (opt_new_filament != nullptr && ! opt_new_filament->is_nil()) {
// An extruder retract override is available at some of the filament presets.
bool overriden = opt_new->overriden_by(opt_new_filament);
if (overriden || *opt_old != *opt_new) {
bool printer_config_changed = (*opt_old != *opt_new);
if (overriden || printer_config_changed) {
auto opt_copy = opt_new->clone();
if (!((opt_key == "long_retractions_when_cut" || opt_key == "retraction_distances_when_cut")
&& new_full_config.option<ConfigOptionInt>("enable_long_retraction_when_cut")->value != LongRectrationLevel::EnableFilament)) // ugly code, remove it later if firmware supports
opt_copy->apply_override(opt_new_filament);
bool changed = *opt_old != *opt_copy;
if (changed)
print_diff.emplace_back(opt_key);
if (changed || overriden) {
// If user directly edited printer config, don't override it with filament values
if ((changed || overriden) && !printer_config_changed) {
if ((opt_key == "long_retractions_when_cut" || opt_key == "retraction_distances_when_cut")
&& new_full_config.option<ConfigOptionInt>("enable_long_retraction_when_cut")->value != LongRectrationLevel::EnableFilament)
continue;
// filament_overrides will be applied to the placeholder parser, which layers these parameters over full_print_config.
filament_overrides.set_key_value(opt_key, opt_copy);
} else
} else if (changed && printer_config_changed) {
// Only add to print_diff, not to filament_overrides
// This preserves user's printer config edit
BOOST_LOG_TRIVIAL(info) << "print_config_diffs: " << opt_key
<< " - printer config changed, not adding to filament_overrides to preserve user edit";
delete opt_copy;
} else {
delete opt_copy;
}
}
} else if (*opt_new != *opt_old) {
//BBS: add plate_index logic for wipe_tower_x/wipe_tower_y
@@ -1094,6 +1271,10 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
// BBS
int used_filaments = this->extruders(true).size();
// 此时 m_objects 和 m_config 是稳定的,可以安全地计算映射
// 这确保了 print_config_diffs 中的参数继承机制能正常工作
this->initialize_filament_extruder_map();
//new_full_config.normalize_fdm(used_filaments);
new_full_config.normalize_fdm_1();
t_config_option_keys changed_keys = new_full_config.normalize_fdm_2(objects().size(), used_filaments);
@@ -1131,12 +1312,36 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
// Find modified keys of the various configs. Resolve overrides extruder retract values by filament profiles.
DynamicPrintConfig filament_overrides;
//BBS: add plate index
t_config_option_keys print_diff = print_config_diffs(m_config, new_full_config, filament_overrides, this->m_plate_index);
t_config_option_keys print_diff = print_config_diffs(m_config, new_full_config, filament_overrides, this->m_plate_index, m_filament_extruder_map);
t_config_option_keys full_config_diff = full_print_config_diffs(m_full_print_config, new_full_config, this->m_plate_index);
// Collect changes to object and region configs.
t_config_option_keys object_diff = m_default_object_config.diff(new_full_config);
t_config_option_keys region_diff = m_default_region_config.diff(new_full_config);
//
// 问题根源:
// 1. 回抽参数(如retraction_length)既存在于打印机配置,也可能被耗材覆盖
// 2. 当耗材-挤出机映射激活时(m_filament_extruder_map不为空),
// filament_overrides中的回抽值会覆盖用户对打印机配置的直接修改
//
// 修复策略:
// - 当存在耗材-挤出机映射时(说明已加载项目),移除filament_overrides中的所有回抽参数
// - 这样用户的打印机配置修改就不会被耗材覆盖值覆盖
// - 保留filament_overrides中其他参数的功能不受影响
//
// 注意:此修复确保用户对打印机挤出机回抽参数的直接编辑拥有最高优先级
const std::vector<std::string> &extruder_retract_keys = print_config_def.extruder_retract_keys();
bool has_mapping = !m_filament_extruder_map.empty();
if (has_mapping) {
// 当有映射时,移除所有回抽参数的耗材覆盖,让打印机配置生效
for (const std::string &key : extruder_retract_keys) {
if (filament_overrides.erase(key)) {
BOOST_LOG_TRIVIAL(info) << "Print::apply - Clearing filament override for '" << key
<< "' to allow printer config to take effect (filament-extruder mapping active)";
}
}
}
// Do not use the ApplyStatus as we will use the max function when updating apply_status.
unsigned int apply_status = APPLY_STATUS_UNCHANGED;
auto update_apply_status = [&apply_status](bool invalidated)
@@ -1550,6 +1755,10 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
m_full_print_config = std::move(new_full_config);
}
// 在对象同步后,m_objects 可能已经被重建,需要重新计算映射以反映最新状态
// 这确保了后续使用映射表时(如 export_gcode)能获得正确的映射关系
this->initialize_filament_extruder_map();
// All regions now have distinct settings.
// Check whether applying the new region config defaults we would get different regions,
// update regions or create regions from scratch.
+4 -2
View File
@@ -504,8 +504,10 @@ set(SLIC3R_GUI_SOURCES
GUI/SMPhysicalPrinterDialog.cpp
GUI/SSWCP.cpp
GUI/SSWCP.hpp
GUI/WCPDownloadManager.cpp
GUI/WCPDownloadManager.hpp
GUI/DownloadManager.cpp
GUI/DownloadManager.hpp
GUI/GenericDownloadDialog.cpp
GUI/GenericDownloadDialog.hpp
GUI/WebPresetDialog.hpp
GUI/WebPresetDialog.cpp
GUI/WebSMUserLoginDialog.cpp
+1
View File
@@ -57,6 +57,7 @@ BBLStatusBarSend::BBLStatusBarSend(wxWindow *parent, int id)
m_cancelbutton->SetBorderColor(btn_bd_white);
m_cancelbutton->SetTextColor(btn_txt_white);
m_cancelbutton->SetCornerRadius(m_self->FromDIP(12));
m_cancelbutton->SetCursor(wxCURSOR_HAND);
m_cancelbutton->Bind(wxEVT_BUTTON,
[this](wxCommandEvent &evt) {
m_was_cancelled = true;
+579
View File
@@ -0,0 +1,579 @@
#include "DownloadManager.hpp"
#include "GUI_App.hpp"
#include "libslic3r/Utils.hpp"
#include <boost/filesystem.hpp>
#include <boost/nowide/fstream.hpp>
#include <boost/log/trivial.hpp>
#include <boost/format.hpp>
#include <vector>
#include <ctime>
namespace Slic3r { namespace GUI {
// ============================================================================
// Helper Functions
// ============================================================================
std::string DownloadManager::get_unique_file_path(const boost::filesystem::path& file_path)
{
// file_path should be the complete absolute path: directory + filename
std::string original_path = file_path.string();
BOOST_LOG_TRIVIAL(debug) << boost::format("DownloadManager::get_unique_file_path: Checking path '%1%'") % original_path;
// Check if file exists, if not return original path
if (!boost::filesystem::exists(file_path)) {
BOOST_LOG_TRIVIAL(debug) << boost::format("DownloadManager::get_unique_file_path: File does not exist, returning original path '%1%'") % original_path;
return original_path;
}
BOOST_LOG_TRIVIAL(debug) << boost::format("DownloadManager::get_unique_file_path: File exists, generating unique name");
boost::filesystem::path parent_dir = file_path.parent_path();
std::string filename = file_path.filename().string();
std::string extension = file_path.extension().string();
std::string name_without_ext;
if (extension.empty()) {
name_without_ext = filename;
BOOST_LOG_TRIVIAL(debug) << boost::format("DownloadManager::get_unique_file_path: No extension found, filename='%1%'") % filename;
} else {
name_without_ext = filename.substr(0, filename.size() - extension.size());
BOOST_LOG_TRIVIAL(debug) << boost::format("DownloadManager::get_unique_file_path: filename='%1%', extension='%2%', name_without_ext='%3%'")
% filename % extension % name_without_ext;
}
// Generate unique filename with Windows-style numbering: filename(1).ext, filename(2).ext, etc.
size_t version = 1;
boost::filesystem::path unique_path;
do {
std::string new_filename;
if (extension.empty()) {
// No extension: filename(1), filename(2), etc.
new_filename = name_without_ext + "(" + std::to_string(version) + ")";
} else {
// Has extension: filename(1).ext, filename(2).ext, etc.
new_filename = name_without_ext + "(" + std::to_string(version) + ")" + extension;
}
unique_path = parent_dir / new_filename;
BOOST_LOG_TRIVIAL(debug) << boost::format("DownloadManager::get_unique_file_path: Trying version %1%: '%2%'") % version % unique_path.string();
version++;
} while (boost::filesystem::exists(unique_path) && version < 10000); // Safety limit
if (version >= 10000) {
// If we hit the limit, log a warning and return a timestamp-based name
BOOST_LOG_TRIVIAL(warning) << boost::format("DownloadManager::get_unique_file_path: Too many duplicate files for '%1%', using timestamp-based name")
% original_path;
std::string timestamp = std::to_string(std::time(nullptr));
std::string new_filename;
if (extension.empty()) {
new_filename = name_without_ext + "_" + timestamp;
} else {
new_filename = name_without_ext + "_" + timestamp + extension;
}
unique_path = parent_dir / new_filename;
}
std::string result = unique_path.string();
BOOST_LOG_TRIVIAL(debug) << boost::format("DownloadManager::get_unique_file_path: Final unique path: '%1%'") % result;
return result;
}
// ============================================================================
// WCP Download Interface (for Web-to-PC communication)
// ============================================================================
size_t DownloadManager::start_wcp_download(const std::string& file_url,
const std::string& file_name,
std::shared_ptr<SSWCP_Instance> wcp_instance,
bool use_original_event_id) {
std::lock_guard<std::mutex> lock(m_tasks_mutex);
size_t task_id = m_next_task_id++;
auto downloadPath = wxGetApp().app_config->get("download_path");
boost::filesystem::path dest_folder(downloadPath);
boost::filesystem::create_directories(dest_folder);
boost::filesystem::path dest_file = dest_folder / file_name;
// Generate unique file path if file already exists
std::string dest_path = get_unique_file_path(dest_file);
// Update file_name if it was changed due to duplicate
std::string actual_file_name = boost::filesystem::path(dest_path).filename().string();
auto task = std::make_shared<DownloadTask>(task_id,
file_url,
actual_file_name,
dest_path,
wcp_instance,
use_original_event_id);
task->state = DownloadTaskState::Downloading;
m_tasks[task_id] = task;
start_download_impl(task);
return task_id;
}
// ============================================================================
// Internal Download Interface (for PC internal use)
// ============================================================================
size_t DownloadManager::start_internal_download(const std::string& file_url,
const std::string& file_name,
const std::string& dest_path,
DownloadCallbacks callbacks) {
std::lock_guard<std::mutex> lock(m_tasks_mutex);
size_t task_id = m_next_task_id++;
boost::filesystem::path dest_path_obj(dest_path);
boost::filesystem::path dest_file_path;
if (boost::filesystem::is_directory(dest_path_obj) || dest_path_obj.filename().empty()) {
// dest_path is a directory, need to append file_name
BOOST_LOG_TRIVIAL(debug) << boost::format("DownloadManager::start_internal_download: dest_path '%1%' is a directory, appending file_name '%2%'")
% dest_path % file_name;
dest_file_path = dest_path_obj / file_name;
} else {
// dest_path is already a complete file path (directory + filename)
BOOST_LOG_TRIVIAL(debug) << boost::format("DownloadManager::start_internal_download: dest_path '%1%' is a complete file path")
% dest_path;
dest_file_path = dest_path_obj;
}
boost::filesystem::create_directories(dest_file_path.parent_path());
std::string unique_dest_path = get_unique_file_path(dest_file_path);
auto task = std::make_shared<DownloadTask>(task_id,
file_url,
file_name,
unique_dest_path,
std::move(callbacks));
task->state = DownloadTaskState::Downloading;
m_tasks[task_id] = task;
start_download_impl(task);
return task_id;
}
size_t DownloadManager::start_internal_download(const std::string& file_url,
const std::string& file_name,
DownloadCallbacks callbacks) {
// Get default download path
auto downloadPath = wxGetApp().app_config->get("download_path");
boost::filesystem::path dest_folder(downloadPath);
boost::filesystem::create_directories(dest_folder);
boost::filesystem::path dest_file = dest_folder / file_name;
// Generate unique file path if file already exists
std::string dest_path = get_unique_file_path(dest_file);
return start_internal_download(file_url, file_name, dest_path, std::move(callbacks));
}
void DownloadManager::start_download_impl(std::shared_ptr<DownloadTask> task) {
wxGetApp().CallAfter([this, task]() {
try {
// Step 1: Create Http object
Http http = Http::get(task->file_url);
http.timeout_max(0);
// Step 2: Set progress callback
http.on_progress([this, task](Http::Progress progress, bool& cancel) {
// Check if task is canceled or already cleaned up
{
std::lock_guard<std::mutex> lock(m_tasks_mutex);
if (m_tasks.find(task->task_id) == m_tasks.end()) {
// Task has been cleaned up, cancel the download
cancel = true;
return;
}
}
if (task->state == DownloadTaskState::Canceled) {
cancel = true;
return;
}
int percent = 0;
if (progress.dltotal > 0) {
percent = (int)(progress.dlnow * 100 / progress.dltotal);
}
task->percent = percent;
// Throttle progress updates: update every 5% or every second
std::lock_guard<std::mutex> lock(m_tasks_mutex);
// Double-check task still exists after acquiring lock
if (m_tasks.find(task->task_id) == m_tasks.end()) {
cancel = true;
return;
}
auto& last_pct = m_last_percent[task->task_id];
auto& last_upd = m_last_update[task->task_id];
auto now = std::chrono::steady_clock::now();
bool should_update = false;
if (percent - last_pct >= 5) {
should_update = true;
last_pct = percent;
} else if (now - last_upd >= std::chrono::seconds(1)) {
should_update = true;
}
if (should_update) {
last_upd = now;
wxGetApp().CallAfter([this, task, percent, progress]() {
// Check if task still exists before sending update
std::lock_guard<std::mutex> lock(m_tasks_mutex);
if (m_tasks.find(task->task_id) != m_tasks.end() &&
task->state != DownloadTaskState::Canceled) {
send_progress_update(task, percent, progress.dlnow, progress.dltotal);
}
});
}
});
// Step 3: Set complete callback
http.on_complete([this, task](std::string body, unsigned status) {
wxGetApp().CallAfter([this, task, body]() {
// Check if task still exists and is not canceled (without lock to avoid deadlock with cleanup_task)
if (task->state == DownloadTaskState::Canceled) {
// Task has been canceled, ignore completion
BOOST_LOG_TRIVIAL(debug) << "DownloadManager: Ignoring complete callback for canceled task " << task->task_id;
return;
}
try {
// Save file
boost::nowide::ofstream file(task->dest_path, std::ios::binary);
if (!file.is_open()) {
std::string error_msg = "Failed to open file for writing: " + task->dest_path;
BOOST_LOG_TRIVIAL(error) << "DownloadManager: " << error_msg;
// Flush logs immediately for critical errors
Slic3r::flush_logs();
send_error_update(task, error_msg);
cleanup_task(task->task_id);
return;
}
file.write(body.c_str(), body.size());
if (file.fail()) {
std::string error_msg = "Failed to write file: " + task->dest_path;
BOOST_LOG_TRIVIAL(error) << "DownloadManager: " << error_msg << ", body size: " << body.size();
// Flush logs immediately for critical errors
Slic3r::flush_logs();
file.close();
send_error_update(task, error_msg);
cleanup_task(task->task_id);
return;
}
file.close();
task->state = DownloadTaskState::Completed;
task->percent = 100;
send_complete_update(task, task->dest_path);
cleanup_task(task->task_id);
} catch (std::exception& e) {
std::string error_msg = std::string("File write exception: ") + e.what();
BOOST_LOG_TRIVIAL(error) << "DownloadManager: " << error_msg << ", file: " << task->dest_path;
// Flush logs immediately for critical errors
Slic3r::flush_logs();
send_error_update(task, error_msg);
cleanup_task(task->task_id);
}
});
});
// Step 4: Set error callback
http.on_error([this, task](std::string body, std::string error, unsigned status) {
wxGetApp().CallAfter([this, task, error, status]() {
// Check if task was canceled (without lock to avoid deadlock with cleanup_task)
if (task->state == DownloadTaskState::Canceled) {
// Task was canceled, ignore error callback (cancel already handled cleanup)
BOOST_LOG_TRIVIAL(debug) << "DownloadManager: Ignoring error callback for canceled task " << task->task_id;
return;
}
std::string error_msg = boost::str(boost::format("HTTP error: %1% (status: %2%)") % error % status);
BOOST_LOG_TRIVIAL(error) << "DownloadManager: " << error_msg
<< ", URL: " << task->file_url
<< ", file: " << task->file_name
<< ", dest: " << task->dest_path;
// Flush logs immediately for critical errors to ensure they are written
Slic3r::flush_logs();
task->state = DownloadTaskState::Error;
task->error_message = error;
send_error_update(task, error);
cleanup_task(task->task_id);
});
});
// Step 5: Start download and save Http::Ptr for cancellation
task->http_object = http.perform();
} catch (std::exception& e) {
std::string error_msg = std::string("Download exception: ") + e.what();
BOOST_LOG_TRIVIAL(error) << "DownloadManager: " << error_msg
<< ", URL: " << task->file_url
<< ", file: " << task->file_name
<< ", dest: " << task->dest_path;
task->state = DownloadTaskState::Error;
task->error_message = e.what();
send_error_update(task, e.what());
cleanup_task(task->task_id);
}
});
}
//this function not currently in use
bool DownloadManager::cancel_download(size_t task_id) {
std::shared_ptr<SSWCP_Instance> wcp_to_destroy;
{
std::lock_guard<std::mutex> lock(m_tasks_mutex);
auto it = m_tasks.find(task_id);
if (it == m_tasks.end()) {
return false;
}
auto task = it->second;
if (task->state == DownloadTaskState::Downloading) {
task->state = DownloadTaskState::Canceled;
if (task->http_object) {
task->http_object->cancel();
}
// Only for WCP downloads
if (task->is_wcp_download()) {
wcp_to_destroy = task->wcp_instance.lock();
} else {
task->callbacks.on_error = nullptr;
task->callbacks.on_progress = nullptr;
task->callbacks.on_complete = nullptr;
}
// Cleanup task directly (already holding the lock, don't call cleanup_task)
m_tasks.erase(task_id);
m_last_percent.erase(task_id);
m_last_update.erase(task_id);
} else {
return false;
}
}
if (wcp_to_destroy) {
wcp_to_destroy->finish_job();
}
return true;
}
// this function not currently in use
bool DownloadManager::pause_download(size_t task_id) {
std::lock_guard<std::mutex> lock(m_tasks_mutex);
auto it = m_tasks.find(task_id);
if (it != m_tasks.end() && it->second->state == DownloadTaskState::Downloading) {
it->second->state = DownloadTaskState::Paused;
return true;
}
return false;
}
// this function not currently in use
bool DownloadManager::resume_download(size_t task_id) {
std::lock_guard<std::mutex> lock(m_tasks_mutex);
auto it = m_tasks.find(task_id);
if (it != m_tasks.end() && it->second->state == DownloadTaskState::Paused) {
return false;
}
return false;
}
// this function not currently in use
DownloadTaskState DownloadManager::get_task_state(size_t task_id) {
std::lock_guard<std::mutex> lock(m_tasks_mutex);
auto it = m_tasks.find(task_id);
if (it != m_tasks.end()) {
return it->second->state;
}
return DownloadTaskState::Error;
}
// this function not currently in use
std::shared_ptr<DownloadTask> DownloadManager::get_task(size_t task_id) {
std::lock_guard<std::mutex> lock(m_tasks_mutex);
auto it = m_tasks.find(task_id);
if (it != m_tasks.end()) {
return it->second;
}
return nullptr;
}
// this function not currently in use
std::vector<std::shared_ptr<DownloadTask>> DownloadManager::get_all_tasks() {
std::lock_guard<std::mutex> lock(m_tasks_mutex);
std::vector<std::shared_ptr<DownloadTask>> result;
result.reserve(m_tasks.size());
for (const auto& pair : m_tasks) {
result.push_back(pair.second);
}
return result;
}
// ============================================================================
// Progress/Complete/Error Update Handlers
// ============================================================================
void DownloadManager::send_progress_update(std::shared_ptr<DownloadTask> task,
int percent,
size_t downloaded,
size_t total) {
if (task->is_wcp_download()) {
send_wcp_progress_update(task, percent, downloaded, total);
} else {
call_internal_progress_callback(task, percent, downloaded, total);
}
}
void DownloadManager::send_wcp_progress_update(std::shared_ptr<DownloadTask> task,
int percent,
size_t downloaded,
size_t total) {
if (!task->use_original_event_id) {
return;
}
if (auto wcp = task->wcp_instance.lock()) {
json progress_data;
progress_data["task_id"] = task->task_id;
progress_data["percent"] = percent;
progress_data["downloaded"] = downloaded;
progress_data["total"] = total;
progress_data["state"] = "downloading";
wcp->m_res_data = progress_data;
wcp->m_status = 0;
wcp->m_msg = "Download progress";
json header;
if (task->use_original_event_id) {
header["event_id"] = wcp->m_event_id;
} else {
header["event_id"] = wcp->m_event_id + "_progress";
}
header["command"] = "download_progress";
wcp->m_header = header;
wcp->send_to_js();
}
}
void DownloadManager::call_internal_progress_callback(std::shared_ptr<DownloadTask> task,
int percent,
size_t downloaded,
size_t total) {
if (task->callbacks.on_progress && task->state != DownloadTaskState::Canceled) {
task->callbacks.on_progress(task->task_id, percent, downloaded, total);
}
}
void DownloadManager::send_complete_update(std::shared_ptr<DownloadTask> task,
const std::string& file_path) {
if (task->is_wcp_download()) {
send_wcp_complete_update(task, file_path);
} else {
call_internal_complete_callback(task, file_path);
}
}
void DownloadManager::send_wcp_complete_update(std::shared_ptr<DownloadTask> task,
const std::string& file_path) {
if (!task->use_original_event_id) {
return;
}
if (auto wcp = task->wcp_instance.lock()) {
json complete_data;
complete_data["task_id"] = task->task_id;
complete_data["file_path"] = file_path;
complete_data["file_name"] = task->file_name;
complete_data["percent"] = 100;
complete_data["state"] = "completed";
wcp->m_res_data = complete_data;
wcp->m_status = 0;
wcp->m_msg = "Download completed";
wcp->send_to_js();
wcp->finish_job();
}
}
void DownloadManager::call_internal_complete_callback(std::shared_ptr<DownloadTask> task,
const std::string& file_path) {
if (task->callbacks.on_complete && task->state != DownloadTaskState::Canceled) {
task->callbacks.on_complete(task->task_id, file_path);
}
}
void DownloadManager::send_error_update(std::shared_ptr<DownloadTask> task,
const std::string& error) {
if (task->is_wcp_download()) {
send_wcp_error_update(task, error);
} else {
call_internal_error_callback(task, error);
}
}
void DownloadManager::send_wcp_error_update(std::shared_ptr<DownloadTask> task,
const std::string& error) {
if (!task->use_original_event_id) {
return;
}
if (auto wcp = task->wcp_instance.lock()) {
json error_data;
error_data["task_id"] = task->task_id;
error_data["error"] = error;
error_data["state"] = "error";
wcp->m_res_data = error_data;
wcp->m_status = -1;
wcp->m_msg = error;
wcp->send_to_js();
wcp->finish_job();
}
}
void DownloadManager::call_internal_error_callback(std::shared_ptr<DownloadTask> task,
const std::string& error) {
if (task->callbacks.on_error && task->state != DownloadTaskState::Canceled) {
task->callbacks.on_error(task->task_id, error);
}
}
void DownloadManager::cleanup_task(size_t task_id) {
std::lock_guard<std::mutex> lock(m_tasks_mutex);
m_tasks.erase(task_id);
m_last_percent.erase(task_id);
m_last_update.erase(task_id);
}
}} // namespace Slic3r::GUI
+210
View File
@@ -0,0 +1,210 @@
#ifndef slic3r_DownloadManager_hpp_
#define slic3r_DownloadManager_hpp_
#include <memory>
#include <string>
#include <unordered_map>
#include <mutex>
#include <atomic>
#include <chrono>
#include <functional>
#include "../Utils/Http.hpp"
#include "SSWCP.hpp"
#include <boost/filesystem/path.hpp>
#include "nlohmann/json.hpp"
namespace Slic3r { namespace GUI {
// Download task state (renamed to avoid conflict with Downloader::DownloadState)
enum class DownloadTaskState {
Pending,
Downloading,
Paused,
Completed,
Error,
Canceled
};
// Download callback interface for internal downloads
struct DownloadCallbacks {
std::function<void(size_t task_id, int percent, size_t downloaded, size_t total)> on_progress;
std::function<void(size_t task_id, const std::string& file_path)> on_complete;
std::function<void(size_t task_id, const std::string& error)> on_error;
DownloadCallbacks() = default;
DownloadCallbacks(
std::function<void(size_t, int, size_t, size_t)> progress,
std::function<void(size_t, const std::string&)> complete,
std::function<void(size_t, const std::string&)> error)
: on_progress(std::move(progress))
, on_complete(std::move(complete))
, on_error(std::move(error))
{}
};
// Download task information
struct DownloadTask {
size_t task_id;
std::string file_url;
std::string file_name;
std::string dest_path;
std::weak_ptr<SSWCP_Instance> wcp_instance;
DownloadCallbacks callbacks;
Http::Ptr http_object;
DownloadTaskState state;
int percent;
std::string error_message;
bool auto_finish_job;
bool use_original_event_id;
// Constructor for WCP downloads
DownloadTask(size_t id, const std::string& url, const std::string& name,
const std::string& path, std::shared_ptr<SSWCP_Instance> instance,
bool use_original_event = false)
: task_id(id), file_url(url), file_name(name), dest_path(path)
, wcp_instance(instance), state(DownloadTaskState::Pending), percent(0)
, auto_finish_job(false), use_original_event_id(use_original_event)
{}
// Constructor for internal downloads
DownloadTask(size_t id, const std::string& url, const std::string& name,
const std::string& path, DownloadCallbacks cb)
: task_id(id), file_url(url), file_name(name), dest_path(path)
, callbacks(std::move(cb)), state(DownloadTaskState::Pending), percent(0)
, auto_finish_job(false), use_original_event_id(false)
{}
// Check if this is a WCP download
bool is_wcp_download() const {
return !wcp_instance.expired();
}
};
class DownloadManager {
public:
static DownloadManager& getInstance() {
static DownloadManager instance;
return instance;
}
// ============================================================================
// WCP Download Interface (for Web-to-PC communication)
// ============================================================================
size_t start_wcp_download(const std::string& file_url,
const std::string& file_name,
std::shared_ptr<SSWCP_Instance> wcp_instance,
bool use_original_event_id = false);
// ============================================================================
// Internal Download Interface (for PC internal use)
// ============================================================================
size_t start_internal_download(const std::string& file_url,
const std::string& file_name,
const std::string& dest_path,
DownloadCallbacks callbacks);
size_t start_internal_download(const std::string& file_url,
const std::string& file_name,
DownloadCallbacks callbacks);
// ============================================================================
// Common Interface (works for both WCP and internal downloads)
// ============================================================================
// Cancel a download task
bool cancel_download(size_t task_id);
// Pause a download task (if needed)
bool pause_download(size_t task_id);
// Resume a download task (if needed)
bool resume_download(size_t task_id);
// Get task state
DownloadTaskState get_task_state(size_t task_id);
// Get task information
std::shared_ptr<DownloadTask> get_task(size_t task_id);
// Get all active tasks
std::vector<std::shared_ptr<DownloadTask>> get_all_tasks();
private:
DownloadManager() = default;
~DownloadManager() = default;
DownloadManager(const DownloadManager&) = delete;
DownloadManager& operator=(const DownloadManager&) = delete;
std::mutex m_tasks_mutex;
std::unordered_map<size_t, std::shared_ptr<DownloadTask>> m_tasks;
std::atomic<size_t> m_next_task_id{1};
// Track last progress update for throttling
std::unordered_map<size_t, int> m_last_percent;
std::unordered_map<size_t, std::chrono::steady_clock::time_point> m_last_update;
// ============================================================================
// Internal Implementation
// ============================================================================
// Common download implementation (used by both WCP and internal downloads)
void start_download_impl(std::shared_ptr<DownloadTask> task);
// Send progress update (handles both WCP and internal modes)
void send_progress_update(std::shared_ptr<DownloadTask> task,
int percent,
size_t downloaded,
size_t total);
// Send completion message (handles both WCP and internal modes)
void send_complete_update(std::shared_ptr<DownloadTask> task,
const std::string& file_path);
// Send error message (handles both WCP and internal modes)
void send_error_update(std::shared_ptr<DownloadTask> task,
const std::string& error);
// WCP-specific: Send progress update via WCP instance
void send_wcp_progress_update(std::shared_ptr<DownloadTask> task,
int percent,
size_t downloaded,
size_t total);
// WCP-specific: Send completion via WCP instance
void send_wcp_complete_update(std::shared_ptr<DownloadTask> task,
const std::string& file_path);
// WCP-specific: Send error via WCP instance
void send_wcp_error_update(std::shared_ptr<DownloadTask> task,
const std::string& error);
// Internal-specific: Call progress callback
void call_internal_progress_callback(std::shared_ptr<DownloadTask> task,
int percent,
size_t downloaded,
size_t total);
// Internal-specific: Call complete callback
void call_internal_complete_callback(std::shared_ptr<DownloadTask> task,
const std::string& file_path);
// Internal-specific: Call error callback
void call_internal_error_callback(std::shared_ptr<DownloadTask> task,
const std::string& error);
// Clean up completed task
void cleanup_task(size_t task_id);
// Generate unique file path if file already exists
// Returns path like "file(1).zip", "file(2).zip" etc.
static std::string get_unique_file_path(const boost::filesystem::path& file_path);
};
}} // namespace Slic3r::GUI
#endif // slic3r_DownloadManager_hpp_
+2 -2
View File
@@ -136,12 +136,12 @@ void change_opt_value(DynamicPrintConfig& config, const t_config_option_key& opt
}
case coPercents:{
ConfigOptionPercents* vec_new = new ConfigOptionPercents{ boost::any_cast<double>(value) };
config.option<ConfigOptionPercents>(opt_key)->set_at(vec_new, opt_index, opt_index);
config.option<ConfigOptionPercents>(opt_key)->set_at(vec_new, opt_index, 0); // SM Orca: Fix - use src_idx=0 for single-element vectors
break;
}
case coFloats:{
ConfigOptionFloats* vec_new = new ConfigOptionFloats{ boost::any_cast<double>(value) };
config.option<ConfigOptionFloats>(opt_key)->set_at(vec_new, opt_index, opt_index);
config.option<ConfigOptionFloats>(opt_key)->set_at(vec_new, opt_index, 0); // SM Orca: Fix - use src_idx=0 for single-element vectors
break;
}
case coString:
+9 -16
View File
@@ -13,7 +13,7 @@
#include "slic3r/GUI/WebPresetDialog.hpp"
#include "slic3r/GUI/SSWCP.hpp"
#include "slic3r/GUI/WCPDownloadManager.hpp"
#include "slic3r/GUI/DownloadManager.hpp"
#include "slic3r/Utils/PresetUpdater.hpp"
#include "slic3r/Config/Version.hpp"
@@ -1066,7 +1066,7 @@ GUI_App::GUI_App()
, m_imgui(new ImGuiWrapper())
, m_removable_drive_manager(std::make_unique<RemovableDriveManager>())
, m_downloader(std::make_unique<Downloader>())
, m_wcp_download_manager(&WCPDownloadManager::getInstance())
, m_download_manager(&DownloadManager::getInstance())
, m_other_instance_message_handler(std::make_unique<OtherInstanceMessageHandler>())
{
//app config initializes early becasuse it is used in instance checking in Snapmaker_Orca.cpp
@@ -1082,7 +1082,9 @@ GUI_App::GUI_App()
m_page_http_server.setPort(PAGE_HTTP_PORT);
m_page_http_server.set_request_handler(HttpServer::web_server_handle_request);
m_page_http_server.start();
BOOST_LOG_TRIVIAL(info) << "[Flutter] Version:"<<common::get_flutter_version();
BOOST_LOG_TRIVIAL(info) << "[Profile] Version:" << common::get_profile_version();
flush_logs();
m_fltviews.set_app(this);
}
@@ -4895,7 +4897,7 @@ void GUI_App::check_new_version_sf(bool show_tips, bool by_user)
BOOST_LOG_TRIVIAL(fatal) << "request server soft update data error:" << errorMsg;
}
})
.perform_sync();
.perform();
}
void GUI_App::process_network_msg(std::string dev_id, std::string msg)
{
@@ -6482,9 +6484,9 @@ Downloader* GUI_App::downloader()
return m_downloader.get();
}
WCPDownloadManager* GUI_App::wcp_download_manager()
DownloadManager* GUI_App::download_manager()
{
return m_wcp_download_manager;
return m_download_manager;
}
void GUI_App::load_url(wxString url)
@@ -6979,16 +6981,7 @@ bool GUI_App::config_wizard_startup()
BOOST_LOG_TRIVIAL(info) << "finished run wizard";
return true;
} /*else if (get_app_config()->legacy_datadir()) {
// Looks like user has legacy pre-vendorbundle data directory,
// explain what this is and run the wizard
MsgDataLegacy dlg;
dlg.ShowModal();
run_wizard(ConfigWizard::RR_DATA_LEGACY);
return true;
}*/
}
if (isAgree.empty())
{
+3 -3
View File
@@ -89,7 +89,7 @@ class Plater;
class ParamsPanel;
class NotificationManager;
class Downloader;
class WCPDownloadManager;
class DownloadManager;
struct GUI_InitParams;
class ParamsDialog;
class HMSQuery;
@@ -298,7 +298,7 @@ private:
size_t m_instance_hash_int;
std::unique_ptr<Downloader> m_downloader;
WCPDownloadManager* m_wcp_download_manager;
DownloadManager* m_download_manager;
//BBS
bool m_is_closing {false};
@@ -686,7 +686,7 @@ private:
Model& model();
NotificationManager * notification_manager();
Downloader* downloader();
WCPDownloadManager* wcp_download_manager();
DownloadManager* download_manager();
std::string m_mall_model_download_url;
+434
View File
@@ -0,0 +1,434 @@
#include "GenericDownloadDialog.hpp"
#include <wx/settings.h>
#include <wx/sizer.h>
#include <wx/stattext.h>
#include <wx/button.h>
#include <wx/hyperlink.h>
#include <wx/textctrl.h>
#include <wx/scrolwin.h>
#include <wx/event.h>
#include <wx/dcgraph.h>
#include <boost/log/trivial.hpp>
#include <boost/format.hpp>
#include <boost/algorithm/string/replace.hpp>
#include <boost/filesystem.hpp>
#include "libslic3r/libslic3r.h"
#include "libslic3r/Utils.hpp"
#include "GUI.hpp"
#include "I18N.hpp"
#include "wxExtensions.hpp"
#include "slic3r/GUI/MainFrame.hpp"
#include "GUI_App.hpp"
#include "slic3r/GUI/DownloadManager.hpp"
namespace Slic3r {
namespace GUI {
GenericDownloadDialog::GenericDownloadDialog(wxString title,
const std::string& file_url,
const std::string& file_name,
const std::string& dest_path,
wxWindow* parent)
: DPIDialog(parent ? parent : static_cast<wxWindow *>(wxGetApp().mainframe),
wxID_ANY, title, wxDefaultPosition, wxDefaultSize, wxCAPTION | wxCLOSE_BOX)
, m_title(title)
, m_file_url(file_url)
, m_file_name(file_name)
, m_dest_path(dest_path)
{
std::string icon_path = (boost::format("%1%/images/Snapmaker_OrcaTitle.ico") % resources_dir()).str();
SetIcon(wxIcon(encode_path(icon_path.c_str()), wxBITMAP_TYPE_ICO));
SetBackgroundColour(*wxWHITE);
setup_ui();
Bind(wxEVT_CLOSE_WINDOW, &GenericDownloadDialog::on_close, this);
wxGetApp().UpdateDlgDarkUI(this);
}
GenericDownloadDialog::~GenericDownloadDialog()
{
// Set destroying flag first to prevent any callbacks from accessing this object
m_is_destroying = true;
// Cancel any active download before destruction
if (m_task_id > 0) {
DownloadManager::getInstance().cancel_download(m_task_id);
m_task_id = 0;
}
}
void GenericDownloadDialog::setup_ui()
{
wxBoxSizer *m_sizer_main = new wxBoxSizer(wxVERTICAL);
auto m_line_top = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(-1, 1));
m_line_top->SetBackgroundColour(wxColour(166, 169, 170));
m_sizer_main->Add(m_line_top, 0, wxEXPAND, 0);
m_simplebook_status = new wxSimplebook(this);
m_simplebook_status->SetSize(wxSize(FromDIP(420), FromDIP(100)));
m_simplebook_status->SetMinSize(wxSize(FromDIP(420), FromDIP(100)));
m_simplebook_status->SetMaxSize(wxSize(FromDIP(420), FromDIP(250)));
// Progress page
m_status_bar = std::make_shared<BBLStatusBarSend>(m_simplebook_status);
m_panel_download = m_status_bar->get_panel();
m_panel_download->SetSize(wxSize(FromDIP(400), FromDIP(70)));
m_panel_download->SetMinSize(wxSize(FromDIP(400), FromDIP(70)));
m_panel_download->SetMaxSize(wxSize(FromDIP(400), FromDIP(70)));
// Complete page
m_panel_complete = new wxPanel(m_simplebook_status, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL);
wxBoxSizer* sizer_complete = new wxBoxSizer(wxVERTICAL);
m_complete_text = new wxStaticText(m_panel_complete, wxID_ANY, _L("Download completed successfully!"),
wxDefaultPosition, wxDefaultSize, 0);
m_complete_text->SetForegroundColour(*wxBLACK);
m_complete_text->Wrap(FromDIP(360));
sizer_complete->Add(m_complete_text, 0, wxALIGN_CENTER | wxALL, 5);
StateColor btn_close_bg(std::pair<wxColour, int>(wxColour(0x90, 0x90, 0x90), StateColor::Disabled),
std::pair<wxColour, int>(wxColour(206, 206, 206), StateColor::Pressed),
std::pair<wxColour, int>(wxColour(238, 238, 238), StateColor::Hovered),
std::pair<wxColour, int>(wxColour(231, 231, 231), StateColor::Normal));
StateColor btn_close_bd(std::pair<wxColour, int>(wxColour(255, 255, 254), StateColor::Disabled),
std::pair<wxColour, int>(wxColour(38, 46, 48), StateColor::Enabled));
StateColor btn_close_txt(std::pair<wxColour, int>(wxColour("#FFFFFE"), StateColor::Disabled),
std::pair<wxColour, int>(wxColour(36, 36, 36), StateColor::Normal));
m_close_button = new Button(m_panel_complete, _L("Close"));
m_close_button->SetSize(wxSize(FromDIP(80), FromDIP(28)));
m_close_button->SetMinSize(wxSize(FromDIP(80), FromDIP(28)));
m_close_button->SetMaxSize(wxSize(FromDIP(80), FromDIP(28)));
m_close_button->SetBackgroundColour(*wxWHITE);
m_close_button->SetBackgroundColor(btn_close_bg);
m_close_button->SetBorderColor(btn_close_bd);
m_close_button->SetTextColor(btn_close_txt);
m_close_button->SetCornerRadius(FromDIP(12));
m_close_button->SetCursor(wxCURSOR_HAND);
m_close_button->Bind(wxEVT_BUTTON, &GenericDownloadDialog::on_close_clicked, this);
sizer_complete->Add(m_close_button, 0, wxALIGN_CENTER | wxALL, 5);
m_panel_complete->SetSizer(sizer_complete);
m_panel_complete->Layout();
sizer_complete->Fit(m_panel_complete);
// Error page
m_panel_error = new wxPanel(m_simplebook_status, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL);
wxBoxSizer* sizer_error = new wxBoxSizer(wxVERTICAL);
// Simple label for error message display
m_error_text = new wxStaticText(m_panel_error, wxID_ANY, wxEmptyString,
wxDefaultPosition, wxSize(FromDIP(380), -1),
wxALIGN_LEFT | wxST_ELLIPSIZE_END);
m_error_text->SetForegroundColour(*wxBLACK);
m_error_text->Wrap(FromDIP(380));
sizer_error->Add(m_error_text, 0, wxEXPAND | wxLEFT | wxTOP | wxRIGHT, FromDIP(10));
// Button sizer aligned to right
wxBoxSizer* sizer_buttons = new wxBoxSizer(wxHORIZONTAL);
sizer_buttons->AddStretchSpacer();
StateColor btn_retry_bg(std::pair<wxColour, int>(wxColour(255, 255, 255), StateColor::Disabled),
std::pair<wxColour, int>(wxColour(206, 206, 206), StateColor::Pressed),
std::pair<wxColour, int>(wxColour(23, 99, 226), StateColor::Hovered), // Same as Normal
std::pair<wxColour, int>(wxColour(23, 99, 226), StateColor::Normal));
// Set border color to same as background to avoid corner color issues
StateColor btn_retry_bd(std::pair<wxColour, int>(wxColour(255, 255, 255), StateColor::Disabled),
std::pair<wxColour, int>(wxColour(23, 99, 226), StateColor::Enabled)); // Same as background
StateColor btn_retry_txt(std::pair<wxColour, int>(wxColour("#FFFFFE"), StateColor::Disabled),
std::pair<wxColour, int>(wxColour(255, 255, 255), StateColor::Normal));
m_retry_button = new Button(m_panel_error, _L("Retry"));
m_retry_button->SetSize(wxSize(FromDIP(80), FromDIP(28)));
m_retry_button->SetMinSize(wxSize(FromDIP(80), FromDIP(28)));
m_retry_button->SetMaxSize(wxSize(FromDIP(80), FromDIP(28)));
// Set window background color to white to ensure rounded corners are white
m_retry_button->SetBackgroundColour(*wxWHITE);
m_retry_button->SetBackgroundColor(btn_retry_bg);
m_retry_button->SetBorderColor(btn_retry_bg);
m_retry_button->SetTextColor(btn_retry_txt);
m_retry_button->SetCornerRadius(FromDIP(12));
m_retry_button->SetCursor(wxCURSOR_HAND);
m_retry_button->Bind(wxEVT_BUTTON, &GenericDownloadDialog::on_retry_clicked, this);
// Setup StateColor for determine button (gray style)
StateColor btn_determine_bg(std::pair<wxColour, int>(wxColour(255, 255, 255), StateColor::Disabled),
std::pair<wxColour, int>(wxColour(206, 206, 206), StateColor::Pressed),
std::pair<wxColour, int>(wxColour(238, 238, 238), StateColor::Hovered),
std::pair<wxColour, int>(wxColour(231, 231, 231), StateColor::Normal));
StateColor btn_determine_bd(std::pair<wxColour, int>(wxColour(255, 255, 255), StateColor::Disabled),
std::pair<wxColour, int>(wxColour(38, 46, 48), StateColor::Enabled));
StateColor btn_determine_txt(std::pair<wxColour, int>(wxColour("#FFFFFE"), StateColor::Disabled),
std::pair<wxColour, int>(wxColour(36, 36, 36), StateColor::Normal));
m_error_close_button = new Button(m_panel_error, _L("Determine"));
m_error_close_button->SetSize(wxSize(FromDIP(80), FromDIP(28)));
m_error_close_button->SetMinSize(wxSize(FromDIP(80), FromDIP(28)));
m_error_close_button->SetMaxSize(wxSize(FromDIP(80), FromDIP(28)));
// Set window background color to white to ensure rounded corners are white
m_error_close_button->SetBackgroundColour(*wxWHITE);
m_error_close_button->SetBackgroundColor(btn_determine_bg);
m_error_close_button->SetBorderColor(btn_determine_bg);
m_error_close_button->SetTextColor(btn_determine_txt);
m_error_close_button->SetCornerRadius(FromDIP(12));
m_error_close_button->SetCursor(wxCURSOR_HAND);
m_error_close_button->Bind(wxEVT_BUTTON, &GenericDownloadDialog::on_close_clicked, this);
sizer_buttons->Add(m_error_close_button, 0, 0);
sizer_buttons->AddSpacer(FromDIP(6));
sizer_buttons->Add(m_retry_button, 0, 0);
sizer_error->AddSpacer(FromDIP(40));
sizer_error->Add(sizer_buttons, 0, wxALIGN_RIGHT | wxRIGHT, FromDIP(24));
m_panel_error->SetSizer(sizer_error);
m_panel_error->Layout();
sizer_error->Fit(m_panel_error);
m_sizer_main->Add(m_simplebook_status, 0, wxALL, FromDIP(16));
m_simplebook_status->AddPage(m_panel_download, wxEmptyString, true);
m_simplebook_status->AddPage(m_panel_complete, wxEmptyString, false);
m_simplebook_status->AddPage(m_panel_error, wxEmptyString, false);
SetSizer(m_sizer_main);
Layout();
Fit();
CentreOnParent();
}
wxString GenericDownloadDialog::format_text(wxStaticText* st, wxString str, int warp)
{
if (wxGetApp().app_config->get("language") != "zh_CN") {
return str;
}
wxString out_txt = str;
wxString count_txt = "";
for (int i = 0; i < str.length(); i++) {
auto text_size = st->GetTextExtent(count_txt);
if (text_size.x < warp) {
count_txt += str[i];
} else {
out_txt.insert(i - 1, '\n');
count_txt = "";
}
}
return out_txt;
}
void GenericDownloadDialog::start_download()
{
show_progress_page();
m_status_bar->set_progress(0);
m_status_bar->set_status_text(_L("Preparing download..."));
m_status_bar->change_button_label(_L("Cancel"));
m_status_bar->set_cancel_callback_fina([this]() {
if (m_task_id > 0) {
DownloadManager::getInstance().cancel_download(m_task_id);
m_task_id = 0;
}
EndModal(wxID_CANCEL);
});
// Create download callbacks
DownloadCallbacks callbacks;
callbacks.on_progress = [this](size_t task_id, int percent, size_t downloaded, size_t total) {
on_download_progress(task_id, percent, downloaded, total);
};
callbacks.on_complete = [this](size_t task_id, const std::string& file_path) {
on_download_complete(task_id, file_path);
};
callbacks.on_error = [this](size_t task_id, const std::string& error) {
on_download_error(task_id, error);
};
// Start download
if (m_dest_path.empty()) {
m_task_id = DownloadManager::getInstance().start_internal_download(
m_file_url, m_file_name, std::move(callbacks));
} else {
m_task_id = DownloadManager::getInstance().start_internal_download(
m_file_url, m_file_name, m_dest_path, std::move(callbacks));
}
}
int GenericDownloadDialog::ShowModal()
{
start_download();
return DPIDialog::ShowModal();
}
void GenericDownloadDialog::on_download_progress(size_t task_id, int percent, size_t downloaded, size_t total)
{
wxGetApp().CallAfter([this, percent, downloaded, total]() {
// Check if dialog is being destroyed
if (m_is_destroying || IsBeingDeleted()) {
return;
}
update_progress(percent);
// Format status text
wxString status_text;
if (total > 0) {
double downloaded_mb = downloaded / (1024.0 * 1024.0);
double total_mb = total / (1024.0 * 1024.0);
status_text = wxString::Format(_L("Downloading: %.1f MB / %.1f MB (%d%%)"),
downloaded_mb, total_mb, percent);
} else {
double downloaded_mb = downloaded / (1024.0 * 1024.0);
status_text = wxString::Format(_L("Downloading: %.1f MB..."), downloaded_mb);
}
m_status_bar->set_status_text(status_text);
// Call user callback if set
if (m_on_progress) {
m_on_progress(m_task_id, percent, downloaded, total);
}
});
}
void GenericDownloadDialog::on_download_complete(size_t task_id, const std::string& file_path)
{
wxGetApp().CallAfter([this, file_path]() {
// Check if dialog is being destroyed
if (m_is_destroying || IsBeingDeleted()) {
return;
}
m_download_success = true;
m_file_path = file_path;
show_complete_page();
// Mark task as completed - no need to cancel it
m_task_id = 0;
// Call user callback if set
if (m_on_complete) {
m_on_complete(m_task_id, file_path);
}
});
}
void GenericDownloadDialog::on_download_error(size_t task_id, const std::string& error)
{
// Log detailed error information for debugging
BOOST_LOG_TRIVIAL(error) << boost::format("GenericDownloadDialog: Download failed for file '%1%' from URL '%2%'. Error: %3%")
% m_file_name % m_file_url % error;
wxGetApp().CallAfter([this, error]() {
// Check if dialog is being destroyed
if (m_is_destroying || IsBeingDeleted()) {
return;
}
m_download_success = false;
m_error_message = error;
show_error_page(error);
// Mark task as completed (failed) - no need to cancel it
m_task_id = 0;
// Call user callback if set
if (m_on_error) {
m_on_error(m_task_id, error);
}
});
}
void GenericDownloadDialog::on_retry_clicked(wxCommandEvent& event)
{
SetTitle(m_title);
if (m_on_retry) {
m_on_retry();
}
start_download();
event.Skip();
}
void GenericDownloadDialog::on_close_clicked(wxCommandEvent& event)
{
if (m_task_id > 0) {
DownloadManager::getInstance().cancel_download(m_task_id);
m_task_id = 0;
}
EndModal(m_download_success ? wxID_OK : wxID_CANCEL);
event.Skip();
}
void GenericDownloadDialog::on_close(wxCloseEvent& event)
{
if (m_task_id > 0) {
DownloadManager::getInstance().cancel_download(m_task_id);
m_task_id = 0;
}
event.Skip();
}
void GenericDownloadDialog::show_progress_page()
{
m_simplebook_status->SetSelection(0);
m_status_bar->set_progress(0);
m_status_bar->show_cancel_button();
}
void GenericDownloadDialog::show_complete_page()
{
//m_simplebook_status->SetSelection(1);
//m_status_bar->hide_cancel_button();
EndModal(wxID_OK);
}
void GenericDownloadDialog::show_error_page(const std::string& error_msg)
{
m_simplebook_status->SetSelection(2);
SetTitle(_L("Donwload failed"));
// Display simple error message: filename + "Download failed"
wxString filename = wxString::FromUTF8(m_file_name.c_str());
wxString error_text = filename + " - Download failed";
// Set error text in simple label
m_error_text->SetLabel(error_text);
m_error_text->Wrap(FromDIP(380));
m_panel_error->Layout();
m_simplebook_status->Layout();
Layout();
Fit();
m_status_bar->hide_cancel_button();
}
void GenericDownloadDialog::update_progress(int percent, const wxString& status_text)
{
m_status_bar->set_progress(percent);
if (!status_text.IsEmpty()) {
m_status_bar->set_status_text(status_text);
}
}
void GenericDownloadDialog::on_dpi_changed(const wxRect &suggested_rect)
{
// Handle DPI changes if needed
}
}} // namespace Slic3r::GUI
+113
View File
@@ -0,0 +1,113 @@
#ifndef slic3r_GenericDownloadDialog_hpp_
#define slic3r_GenericDownloadDialog_hpp_
#include <string>
#include <functional>
#include <memory>
#include <atomic>
#include "GUI_Utils.hpp"
#include <wx/dialog.h>
#include <wx/simplebook.h>
#include "BBLStatusBar.hpp"
#include "BBLStatusBarSend.hpp"
#include "Jobs/Worker.hpp"
#include "slic3r/GUI/DownloadManager.hpp"
#include "Widgets/Button.hpp"
class wxBoxSizer;
class wxPanel;
class wxStaticText;
class wxHyperlinkCtrl;
namespace Slic3r {
namespace GUI {
// Generic download dialog for custom download tasks with progress display
class GenericDownloadDialog : public DPIDialog
{
public:
// Callback types
using DownloadCallback = std::function<void(size_t task_id, int percent, size_t downloaded, size_t total)>;
using CompleteCallback = std::function<void(size_t task_id, const std::string& file_path)>;
using ErrorCallback = std::function<void(size_t task_id, const std::string& error)>;
using RetryCallback = std::function<void()>;
GenericDownloadDialog(wxString title,
const std::string& file_url,
const std::string& file_name,
const std::string& dest_path = "",
wxWindow* parent = nullptr);
~GenericDownloadDialog();
// Start download
void start_download();
// Set callbacks (optional)
void set_on_progress(DownloadCallback callback) { m_on_progress = callback; }
void set_on_complete(CompleteCallback callback) { m_on_complete = callback; }
void set_on_error(ErrorCallback callback) { m_on_error = callback; }
void set_on_retry(RetryCallback callback) { m_on_retry = callback; }
// Get download result
bool is_success() const { return m_download_success; }
std::string get_file_path() const { return m_file_path; }
std::string get_error_message() const { return m_error_message; }
// Show modal and return result
int ShowModal() override;
protected:
void on_close(wxCloseEvent& event);
void on_dpi_changed(const wxRect &suggested_rect) override;
wxString format_text(wxStaticText* st, wxString str, int warp);
// Event handlers
void on_download_progress(size_t task_id, int percent, size_t downloaded, size_t total);
void on_download_complete(size_t task_id, const std::string& file_path);
void on_download_error(size_t task_id, const std::string& error);
void on_retry_clicked(wxCommandEvent& event);
void on_close_clicked(wxCommandEvent& event);
private:
void setup_ui();
void show_progress_page();
void show_complete_page();
void show_error_page(const std::string& error_msg);
void update_progress(int percent, const wxString& status_text = "");
wxString m_title;
std::string m_file_url;
std::string m_file_name;
std::string m_dest_path;
size_t m_task_id{0};
bool m_download_success{false};
std::string m_file_path;
std::string m_error_message;
// Callbacks
DownloadCallback m_on_progress;
CompleteCallback m_on_complete;
ErrorCallback m_on_error;
RetryCallback m_on_retry;
// UI components
wxSimplebook* m_simplebook_status{nullptr};
std::shared_ptr<BBLStatusBarSend> m_status_bar;
wxPanel* m_panel_download{nullptr};
wxPanel* m_panel_complete{nullptr};
wxPanel* m_panel_error{nullptr};
wxStaticText* m_complete_text{nullptr};
wxStaticText* m_error_text{nullptr};
Button* m_retry_button{nullptr};
Button* m_close_button{nullptr};
Button* m_error_close_button{nullptr};
std::atomic<bool> m_is_destroying{false};
};
}} // namespace Slic3r::GUI
#endif // slic3r_GenericDownloadDialog_hpp_
+44 -2
View File
@@ -75,7 +75,7 @@
#endif // _WIN32
#include <slic3r/GUI/CreatePresetsDialog.hpp>
#include "sentry_wrapper/SentryWrapper.hpp"
#include "GenericDownloadDialog.hpp"
#define UPDATE_BUSER true
#define UPDATE_BUAUTO false
@@ -2260,7 +2260,8 @@ static wxMenu* generate_help_menu()
// //TODO
// });
// Check New Version
append_menu_item(helpMenu, wxID_ANY, _L("Check for Update"), _L("Check for Update"),
append_menu_item(
helpMenu, wxID_ANY, _L("Check for Update"), _L("Check for Update"),
[](wxCommandEvent&) {
wxGetApp().check_new_version_sf(true, UPDATE_BUSER);
}, "", nullptr, []() {
@@ -4020,6 +4021,47 @@ void MainFrame::RunScript(wxString js)
m_webview->RunScript(js);
}
void MainFrame::downloadOpenProject(const std::string& fileUrl, const std::string& fileName, std::string completeFilePath)
{
// std::string fileUrl = "https://public.resource.snapmaker.com/model/public/3mf/test_for_download.3mf";
// std::string filename = "test_for_download.3mf";
GenericDownloadDialog dlg(_L("downloading the model"), fileUrl, fileName, completeFilePath);
auto res = dlg.ShowModal();
if (res != wxID_OK)
return;
if (completeFilePath.empty()) {
auto downloadPath = wxGetApp().app_config->get("download_path");
completeFilePath = downloadPath + "/" + fileName;
}
if (!boost::filesystem::exists(completeFilePath))
{
BOOST_LOG_TRIVIAL(warning) << boost::format("the file '%1%' not exists") % completeFilePath;
return;
}
// Auto-open project if it's a .3mf file
boost::filesystem::path path(completeFilePath);
std::string extension = boost::algorithm::to_lower_copy(path.extension().string());
if (extension == ".3mf") {
BOOST_LOG_TRIVIAL(info) << boost::format("GenericDownloadDialog: Auto-opening project file '%1%'") % completeFilePath;
wxString wx_file_path = wxString::FromUTF8(completeFilePath.c_str());
if (wxGetApp().can_load_project() && wxGetApp().mainframe && wxGetApp().mainframe->plater()) {
wxGetApp().mainframe->plater()->load_project(wx_file_path);
}
}
else
{
// Not a valid 3mf file, show error message
wxString msg = wxString::Format(_L("The downloaded file '%s' is not a valid 3MF project file."), fileName);
MessageDialog(this, msg, _L("Invalid File"), wxOK | wxICON_WARNING).ShowModal();
}
}
void MainFrame::technology_changed()
{
// update menu titles
+5 -1
View File
@@ -348,7 +348,11 @@ public:
void load_printer_url();
bool is_printer_view() const;
void refresh_plugin_tips();
void RunScript(wxString js);
void RunScript(wxString js);
void downloadOpenProject(const std::string& fileUrl,
const std::string& fileName,
std::string completeFilePath = "");
//SoftFever
void show_device(bool bBBLPrinter);
+9 -11
View File
@@ -645,6 +645,10 @@ void ConfigOptionsGroup::on_change_OG(const t_config_option_key& opt_id, const b
const std::string &opt_key = itOption.first;
int opt_index = itOption.second;
// SM Orca: Debug logging - track parameter changes from UI
BOOST_LOG_TRIVIAL(error) << "ConfigOptionsGroup::on_change_OG: opt_id=" << opt_id
<< ", opt_key=" << opt_key << ", opt_index=" << opt_index;
this->change_opt_value(opt_key, value, opt_index == -1 ? 0 : opt_index);
}
@@ -1227,18 +1231,12 @@ void ExtruderOptionsGroup::on_change_OG(const t_config_option_key& opt_id, const
auto itOption = it->second;
const std::string& opt_key = itOption.first;
int opt_index = itOption.second;
auto opt = m_config->option(opt_key);
const ConfigOptionVectorBase* opt_vec = dynamic_cast<const ConfigOptionVectorBase*>(opt);
if (opt_vec != nullptr) {
for (int opt_index = 0; opt_index < opt_vec->size(); opt_index++) {
this->change_opt_value(opt_key, value, opt_index);
}
}
else {
int opt_index = itOption.second;
this->change_opt_value(opt_key, value, opt_index == -1 ? 0 : opt_index);
}
// SM Orca: FIX - Only modify the specific extruder's value, not all extruders
// The original code iterated through all indices and set them to the same value,
// which caused all extruders to have identical values when editing one extruder
this->change_opt_value(opt_key, value, opt_index == -1 ? 0 : opt_index);
}
OptionsGroup::on_change_OG(opt_id, value);
-42
View File
@@ -13743,18 +13743,6 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn, bool us
islegal = (c_preset == connect_preset);
/* if (!islegal) {
MessageDialog msg_window(nullptr,
_L(" Your connected machine is ") + (connect_preset == "" ? "Unknown" : connect_preset) + _L("\nYour model's preset is ") + c_preset + _L("\nDo you want to continue?"),
L("machine check"),
wxICON_QUESTION | wxOK);
int res = msg_window.ShowModal();
if (res != wxID_OK) {
return;
}
}*/
DynamicPrintConfig* physical_printer_config = &Slic3r::GUI::wxGetApp().preset_bundle->printers.get_edited_preset().config;
if (! physical_printer_config || p->model.objects.empty())
return;
@@ -13773,34 +13761,6 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn, bool us
local_name.erase(std::remove(local_name.begin(), local_name.end(), '('), local_name.end());
local_name.erase(std::remove(local_name.begin(), local_name.end(), ')'), local_name.end());
/*if (wxGetApp().app_config->get("use_new_connect") == "true") {
upload_job = PrintHostJob(wxGetApp().get_host_config());
} */
// if (local_name == "Snapmaker U1 0.4 nozzle" && devices.size() == 0) {
// MessageDialog msg_window(nullptr, _L("You don't have active machine, do you want to add one?"), _L("Info"), wxICON_QUESTION | wxOK | wxCANCEL);
// int res = msg_window.ShowModal();
// if (res == wxID_OK) {
// wxGetApp().mainframe->request_select_tab(MainFrame::TabPosition::tpMonitor);
// auto view = wxGetApp().mainframe->m_printer_view;
// if (view) {
// json msg;
// msg["head"] = json::object();
// json payload = json::object();
// payload["cmd"] = "devicepage_add_device";
// payload["method"] = "call_flutter";
// payload["params"] = json::object();
// msg["payload"] = payload;
// std::string str_msg = msg.dump(4, ' ', true);
// view->sendMessage(str_msg);
// }
// }
// return;
// }
if (wxGetApp().app_config->get("use_new_connect") == "true" || local_name == "Snapmaker U1 0.4 nozzle") {
// 先不创建job,直接创建上传 / 上传下载对话框
// 获取默认文件名
@@ -13862,8 +13822,6 @@ void Plater::send_gcode_legacy(int plate_idx, Export3mfProgressFn proFn, bool us
dialog->set_display_file_name(upload_job.upload_data.upload_path.string());
bool res = dialog->run();
// wxGetApp().mainframe->m_printer_view->reload();
if (dialog->is_finish()) {
wxGetApp().mainframe->select_tab(MainFrame::TabPosition::tpMonitor);
}
+86 -37
View File
@@ -2,7 +2,7 @@
#include "SSWCP.hpp"
#include "GUI_App.hpp"
#include "MainFrame.hpp"
#include "WCPDownloadManager.hpp"
#include "DownloadManager.hpp"
#include "nlohmann/json.hpp"
#include "slic3r/GUI/Tab.hpp"
#include "sentry_wrapper/SentryWrapper.hpp"
@@ -3005,10 +3005,11 @@ void SSWCP_MachineOption_Instance::sw_FinishFilamentMapping()
if (wxGetApp().get_web_preprint_dialog()) {
WebPreprintDialog* dialog = dynamic_cast<WebPreprintDialog*>(wxGetApp().get_web_preprint_dialog());
if (dialog) {
// BBS: Use SafeEndModal to prevent duplicate EndModal calls
if(dialog->is_finish()){
dialog->EndModal(wxID_OK);
dialog->SafeEndModal(wxID_OK);
}else{
dialog->EndModal(wxID_CANCEL);
dialog->SafeEndModal(wxID_CANCEL);
}
}
}
@@ -3058,29 +3059,19 @@ void SSWCP_MachineOption_Instance::sw_GetFileFilamentMapping()
long long res = 0;
if ((oriclr.size() != 7 && oriclr.size() != 9) || oriclr[0] != '#') {
return -1;
return 0;
}
if (oriclr.size() == 7) {
for (int i = 1; i <= 6; ++i) {
if (oriclr[7 - i] - '0' >= 0 && oriclr[7 - i] - '0' <= 9) {
res += std::pow(16, i - 1) * (oriclr[7 - i] - '0');
} else {
res += std::pow(16, i - 1) * (oriclr[7 - i] - 'A' + 10);
}
}
} else {
for (int i = 1; i <= 8; ++i) {
if (oriclr[7 - i] - '0' >= 0 && oriclr[7 - i] - '0' <= 9) {
res += std::pow(16, i - 1) * (oriclr[7 - i] - '0');
} else {
res += std::pow(16, i - 1) * (oriclr[7 - i] - 'A' + 10);
}
auto colorSize = oriclr.size();//7 or 9
for (auto i = 1; i < colorSize; i++)
{
if (oriclr[colorSize - i] - '0' >= 0 && oriclr[colorSize - i] - '0' <= 9) {
res += std::pow(16, i - 1) * (oriclr[colorSize - i] - '0');
} else {
res += std::pow(16, i - 1) * (oriclr[colorSize - i] - 'A' + 10);
}
}
return res;
};
@@ -3190,14 +3181,10 @@ void SSWCP_MachineOption_Instance::sw_GetFileFilamentMapping()
response["thumbnails"] = thumbnails;
// file name
response["filename"] = SSWCP::get_display_filename();
response["filepath"] = SSWCP::get_active_filename();
m_res_data = response;
send_to_js();
finish_job();
@@ -4304,6 +4291,8 @@ void SSWCP_UserLogin_Instance::process()
sw_GetUserUpdatePrivacy();
} else if (m_cmd == DOWNLOAD_FILE) {
sw_DownloadFile();
} else if (m_cmd == DOWNLOAD_FILE_AND_OPEN) {
sw_DownloadFileAndOpen();
} else if (m_cmd == CANCEL_DOWNLOAD) {
sw_CancelDownload();
} else if (m_cmd == FILE_VIEW) {
@@ -4390,7 +4379,8 @@ void SSWCP_UserLogin_Instance::sw_GetUserUpdatePrivacy()
}
void SSWCP_UserLogin_Instance::sw_DownloadFile() {
void SSWCP_UserLogin_Instance::sw_DownloadFileAndOpen()
{
try {
std::string fileName = m_param_data.count("file_name") ? m_param_data["file_name"].get<std::string>() : "";
std::string fileUrl = m_param_data.count("file_url") ? m_param_data["file_url"].get<std::string>() : "";
@@ -4400,27 +4390,85 @@ void SSWCP_UserLogin_Instance::sw_DownloadFile() {
return;
}
// Use WCP Download Manager
WCPDownloadManager* download_mgr = wxGetApp().wcp_download_manager();
// Use Download Manager
DownloadManager* download_mgr = wxGetApp().download_manager();
if (!download_mgr) {
handle_general_fail(-1, "WCP Download Manager not available");
handle_general_fail(-1, "Download Manager not available");
return;
}
// Start download task
size_t task_id = download_mgr->start_download(fileUrl, fileName, shared_from_this());
wxGetApp().mainframe->downloadOpenProject(fileUrl, fileName, "");
m_status = 0;
m_msg = "success";
send_to_js();
finish_job();
} catch (std::exception& e) {
handle_general_fail(-1, e.what());
}
}
void SSWCP_UserLogin_Instance::sw_DownloadFile()
{
try {
std::string fileName = m_param_data.count("file_name") ? m_param_data["file_name"].get<std::string>() : "";
std::string fileUrl = m_param_data.count("file_url") ? m_param_data["file_url"].get<std::string>() : "";
if (fileUrl.empty() || fileName.empty()) {
handle_general_fail(-1, "file_url and file_name are required");
return;
}
// Use Download Manager
DownloadManager* download_mgr = wxGetApp().download_manager();
if (!download_mgr) {
handle_general_fail(-1, "Download Manager not available");
return;
}
//only download file and don't do anything.
//wxGetApp().mainframe->downloadOpenProject(fileUrl, fileName, "");
m_status = 0;
m_msg = "success";
send_to_js();
finish_job();
} catch (std::exception& e) {
handle_general_fail(-1, e.what());
}
}
void SSWCP_UserLogin_Instance::sw_DownloadFileEx() {
try {
std::string fileName = m_param_data.count("file_name") ? m_param_data["file_name"].get<std::string>() : "";
std::string fileUrl = m_param_data.count("file_url") ? m_param_data["file_url"].get<std::string>() : "";
if (fileUrl.empty() || fileName.empty()) {
handle_general_fail(-1, "file_url and file_name are required");
return;
}
// Use Download Manager
DownloadManager* download_mgr = wxGetApp().download_manager();
if (!download_mgr) {
handle_general_fail(-1, "Download Manager not available");
return;
}
size_t task_id = download_mgr->start_wcp_download(fileUrl,
fileName,
shared_from_this(),
true);
// Return task ID to Flutter
json response;
response["task_id"] = task_id;
response["file_name"] = fileName;
response["file_url"] = fileUrl;
m_res_data = response;
m_status = 0;
m_msg = "Download started";
m_msg = "success";
send_to_js();
// Note: Do not call finish_job() here, as download is asynchronous
// The manager will send progress updates and completion/error messages via WCP
} catch (std::exception& e) {
handle_general_fail(-1, e.what());
@@ -4436,7 +4484,7 @@ void SSWCP_UserLogin_Instance::sw_CancelDownload() {
return;
}
WCPDownloadManager* download_mgr = wxGetApp().wcp_download_manager();
DownloadManager* download_mgr = wxGetApp().download_manager();
if (!download_mgr) {
handle_general_fail(-1, "WCP Download Manager not available");
return;
@@ -5996,7 +6044,8 @@ std::unordered_set<std::string> SSWCP::m_project_cmd_list = {
};
std::unordered_set<std::string> SSWCP::m_login_cmd_list = {"sw_UserLogin", "sw_UserLogout", "sw_GetUserLoginState", "sw_SubscribeUserLoginState",
UPDATE_PRIVACY_STATUS, GET_PRIVACY_STATUS};
UPDATE_PRIVACY_STATUS, GET_PRIVACY_STATUS,
DOWNLOAD_FILE,FILE_VIEW, CANCEL_DOWNLOAD, DOWNLOAD_FILE_AND_OPEN};
std::unordered_set<std::string> SSWCP::m_machine_manage_cmd_list = {
"sw_GetLocalDevices", "sw_AddDevice", "sw_SubscribeLocalDevices", "sw_RenameDevice", "sw_SwitchModel", "sw_DeleteDevices"
+6
View File
@@ -31,6 +31,7 @@ using tcp = asio::ip::tcp;
#define DELETE_CAMERA_TIMELAPSE "sw_DeleteCameraTimelapse"
#define GET_DEVICEDATA_STORAGESPACE "sw_GetDeviceDataStorageSpace"
#define DOWNLOAD_FILE "sw_DownloadFile"
#define DOWNLOAD_FILE_AND_OPEN "sw_DownLoadFileAndOpen"
#define CANCEL_DOWNLOAD "sw_CancelDownload"
#define FILE_VIEW "sw_FileView"
@@ -541,6 +542,11 @@ private:
void sw_SubUserUpdatePrivacy();
void sw_DownloadFile();
void sw_DownloadFileAndOpen();
void sw_DownloadFileEx();
void sw_CancelDownload();
void sw_FileView();
+40 -4
View File
@@ -3286,7 +3286,24 @@ void TabFilament::add_filament_overrides_page()
else {
const std::string printer_opt_key = opt_key.substr(strlen("filament_"));
const auto printer_config = m_preset_bundle->printers.get_edited_preset().config;
const boost::any printer_config_value = optgroup_sh->get_config_value(printer_config, printer_opt_key, opt_index);
// SM Orca: Map filament slot to physical extruder index for inheritance
auto& filament_extruder_map = wxGetApp().app_config->get_filament_extruder_map_ref();
// SM Orca: First calculate num_extruders to use modulo for default mapping
const ConfigOptionFloats* nozzle_diameter = printer_config.option<ConfigOptionFloats>("nozzle_diameter");
int num_extruders = nozzle_diameter ? (int)nozzle_diameter->values.size() : 1;
// SM Orca: Use modulo arithmetic for default mapping when no explicit mapping exists
int physical_extruder_idx = opt_index % num_extruders; // default: filament N maps to extruder N % num_extruders
auto map_it = filament_extruder_map.find(opt_index);
if (map_it != filament_extruder_map.end()) {
physical_extruder_idx = map_it->second;
}
// SM Orca: Bounds check to prevent crash from misconfigured map
if (physical_extruder_idx < 0 || physical_extruder_idx >= num_extruders) {
BOOST_LOG_TRIVIAL(warning) << "Invalid physical_extruder_idx " << physical_extruder_idx
<< " for filament slot " << opt_index << ", using default";
physical_extruder_idx = std::clamp(physical_extruder_idx, 0, num_extruders - 1);
}
const boost::any printer_config_value = optgroup_sh->get_config_value(printer_config, printer_opt_key, physical_extruder_idx);
field->update_na_value(printer_config_value);
field->set_na_value();
}
@@ -3301,7 +3318,7 @@ void TabFilament::add_filament_overrides_page()
optgroup->append_line(line);
};
const int extruder_idx = 0; // #ys_FIXME
const int extruder_idx = (m_presets_choice && m_presets_choice->get_filament_idx() >= 0) ? m_presets_choice->get_filament_idx() : 0; // SM Orca: Get actual filament slot index
for (const std::string opt_key : { "filament_retraction_length",
"filament_z_hop",
@@ -3367,7 +3384,7 @@ void TabFilament::update_filament_overrides_page(const DynamicPrintConfig* print
// "filament_seam_gap"
};
const int extruder_idx = 0; // #ys_FIXME
const int extruder_idx = (m_presets_choice && m_presets_choice->get_filament_idx() >= 0) ? m_presets_choice->get_filament_idx() : 0; // SM Orca: Get actual filament slot index
const bool have_retract_length = m_config->option("filament_retraction_length")->is_nil() ||
m_config->opt_float("filament_retraction_length", extruder_idx) > 0;
@@ -3399,7 +3416,26 @@ void TabFilament::update_filament_overrides_page(const DynamicPrintConfig* print
} else {
if (!is_checked) {
const std::string printer_opt_key = opt_key.substr(strlen("filament_"));
boost::any printer_config_value = optgroup->get_config_value(*printers_config, printer_opt_key, extruder_idx);
// SM Orca: Map filament slot to physical extruder index for inheritance
auto& filament_extruder_map = wxGetApp().app_config->get_filament_extruder_map_ref();
// SM Orca: Determine extruder count first for proper modulo calculation
const ConfigOptionFloats* nozzle_diameter = printers_config->option<ConfigOptionFloats>("nozzle_diameter");
int num_extruders = nozzle_diameter ? (int)nozzle_diameter->values.size() : 1;
int physical_extruder_idx = extruder_idx; // default: filament N uses extruder N
auto map_it = filament_extruder_map.find(extruder_idx);
if (map_it != filament_extruder_map.end()) {
physical_extruder_idx = map_it->second;
} else {
// SM Orca: Use modulo arithmetic when map entry doesn't exist
physical_extruder_idx = extruder_idx % num_extruders;
}
// SM Orca: Bounds check to prevent crash from misconfigured map
if (physical_extruder_idx < 0 || physical_extruder_idx >= num_extruders) {
BOOST_LOG_TRIVIAL(warning) << "Invalid physical_extruder_idx " << physical_extruder_idx
<< " for filament slot " << extruder_idx << ", using default";
physical_extruder_idx = std::clamp(physical_extruder_idx, 0, num_extruders - 1);
}
boost::any printer_config_value = optgroup->get_config_value(*printers_config, printer_opt_key, physical_extruder_idx);
field->update_na_value(printer_config_value);
field->set_value(printer_config_value, false);
}
-276
View File
@@ -1,276 +0,0 @@
#include "WCPDownloadManager.hpp"
#include "GUI_App.hpp"
#include <boost/filesystem.hpp>
#include <boost/nowide/fstream.hpp>
#include <boost/log/trivial.hpp>
namespace Slic3r { namespace GUI {
size_t WCPDownloadManager::start_download(const std::string& file_url,
const std::string& file_name,
std::shared_ptr<SSWCP_Instance> wcp_instance) {
std::lock_guard<std::mutex> lock(m_tasks_mutex);
size_t task_id = m_next_task_id++;
// Get download path
auto downloadPath = wxGetApp().app_config->get("download_path");
boost::filesystem::path dest_folder(downloadPath);
boost::filesystem::create_directories(dest_folder);
boost::filesystem::path dest_file = dest_folder / file_name;
std::string dest_path = dest_file.string();
// Create task
auto task = std::make_shared<WCPDownloadTask>(task_id, file_url, file_name, dest_path, wcp_instance);
task->state = WCPDownloadState::Downloading;
m_tasks[task_id] = task;
// Start download
wxGetApp().CallAfter([this, task]() {
try {
// Step 1: Create Http object
Http http = Http::get(task->file_url);
// Step 2: Set progress callback
http.on_progress([this, task](Http::Progress progress, bool& cancel) {
if (task->state == WCPDownloadState::Canceled) {
cancel = true;
return;
}
// Calculate progress
int percent = 0;
if (progress.dltotal > 0) {
percent = (int)(progress.dlnow * 100 / progress.dltotal);
}
task->percent = percent;
// Throttle progress updates: update every 5% or every second
std::lock_guard<std::mutex> lock(m_tasks_mutex);
auto& last_pct = m_last_percent[task->task_id];
auto& last_upd = m_last_update[task->task_id];
auto now = std::chrono::steady_clock::now();
bool should_update = false;
if (percent - last_pct >= 5) {
should_update = true;
last_pct = percent;
} else if (now - last_upd >= std::chrono::seconds(1)) {
should_update = true;
}
if (should_update) {
last_upd = now;
wxGetApp().CallAfter([this, task, percent, progress]() {
send_progress_update(task, percent, progress.dlnow, progress.dltotal);
});
}
});
// Step 3: Set complete callback
http.on_complete([this, task](std::string body, unsigned status) {
wxGetApp().CallAfter([this, task, body]() {
try {
// Save file
boost::nowide::ofstream file(task->dest_path, std::ios::binary);
if (!file.is_open()) {
send_error_update(task, "Failed to open file for writing");
cleanup_task(task->task_id);
return;
}
file.write(body.c_str(), body.size());
file.close();
task->state = WCPDownloadState::Completed;
task->percent = 100;
send_complete_update(task, task->dest_path);
cleanup_task(task->task_id);
} catch (std::exception& e) {
send_error_update(task, e.what());
cleanup_task(task->task_id);
}
});
});
// Step 4: Set error callback
http.on_error([this, task](std::string body, std::string error, unsigned status) {
wxGetApp().CallAfter([this, task, error, status]() {
task->state = WCPDownloadState::Error;
task->error_message = error;
send_error_update(task, error);
cleanup_task(task->task_id);
});
});
// Step 5: Start download and save Http::Ptr for cancellation
task->http_object = http.perform();
} catch (std::exception& e) {
task->state = WCPDownloadState::Error;
task->error_message = e.what();
send_error_update(task, e.what());
cleanup_task(task->task_id);
}
});
return task_id;
}
bool WCPDownloadManager::cancel_download(size_t task_id) {
std::shared_ptr<SSWCP_Instance> wcp_to_destroy;
{
std::lock_guard<std::mutex> lock(m_tasks_mutex);
auto it = m_tasks.find(task_id);
if (it == m_tasks.end()) {
return false;
}
auto task = it->second;
if (task->state == WCPDownloadState::Downloading) {
task->state = WCPDownloadState::Canceled;
if (task->http_object) {
task->http_object->cancel();
}
// Get WCP instance before cleanup (for destruction after lock release)
wcp_to_destroy = task->wcp_instance.lock();
cleanup_task(task_id);
} else {
return false;
}
}
// Destroy WCP instance outside the lock to prevent deadlock
// This is the WCP instance from the original download request (sw_DownloadFile)
if (wcp_to_destroy) {
wcp_to_destroy->finish_job();
}
return true;
}
bool WCPDownloadManager::pause_download(size_t task_id) {
// Pause functionality can be implemented if needed
// Current Http module may not support pause, need to implement resume from breakpoint
std::lock_guard<std::mutex> lock(m_tasks_mutex);
auto it = m_tasks.find(task_id);
if (it != m_tasks.end() && it->second->state == WCPDownloadState::Downloading) {
it->second->state = WCPDownloadState::Paused;
// Note: Http module doesn't support pause directly, would need breakpoint resume
return true;
}
return false;
}
bool WCPDownloadManager::resume_download(size_t task_id) {
// Resume functionality can be implemented if needed
// Would require breakpoint resume support in Http module
std::lock_guard<std::mutex> lock(m_tasks_mutex);
auto it = m_tasks.find(task_id);
if (it != m_tasks.end() && it->second->state == WCPDownloadState::Paused) {
// Would need to restart download with range header
return false; // Not implemented yet
}
return false;
}
WCPDownloadState WCPDownloadManager::get_task_state(size_t task_id) {
std::lock_guard<std::mutex> lock(m_tasks_mutex);
auto it = m_tasks.find(task_id);
if (it != m_tasks.end()) {
return it->second->state;
}
return WCPDownloadState::Error;
}
std::shared_ptr<WCPDownloadTask> WCPDownloadManager::get_task(size_t task_id) {
std::lock_guard<std::mutex> lock(m_tasks_mutex);
auto it = m_tasks.find(task_id);
if (it != m_tasks.end()) {
return it->second;
}
return nullptr;
}
void WCPDownloadManager::send_progress_update(std::shared_ptr<WCPDownloadTask> task,
int percent,
size_t downloaded,
size_t total) {
if (auto wcp = task->wcp_instance.lock()) {
json progress_data;
progress_data["task_id"] = task->task_id;
progress_data["percent"] = percent;
progress_data["downloaded"] = downloaded;
progress_data["total"] = total;
progress_data["state"] = "downloading";
wcp->m_res_data = progress_data;
wcp->m_status = 0;
wcp->m_msg = "Download progress";
// Use progress event ID
json header;
header["event_id"] = wcp->m_event_id + "_progress";
header["command"] = "download_progress";
wcp->m_header = header;
wcp->send_to_js();
}
}
void WCPDownloadManager::send_complete_update(std::shared_ptr<WCPDownloadTask> task,
const std::string& file_path) {
if (auto wcp = task->wcp_instance.lock()) {
json complete_data;
complete_data["task_id"] = task->task_id;
complete_data["file_path"] = file_path;
complete_data["file_name"] = task->file_name;
complete_data["percent"] = 100;
complete_data["state"] = "completed";
wcp->m_res_data = complete_data;
wcp->m_status = 0;
wcp->m_msg = "Download completed";
wcp->send_to_js();
// Release WCP instance to prevent memory leak
wcp->finish_job();
}
}
void WCPDownloadManager::send_error_update(std::shared_ptr<WCPDownloadTask> task,
const std::string& error) {
if (auto wcp = task->wcp_instance.lock()) {
json error_data;
error_data["task_id"] = task->task_id;
error_data["error"] = error;
error_data["state"] = "error";
wcp->m_res_data = error_data;
wcp->m_status = -1;
wcp->m_msg = error;
wcp->send_to_js();
// Release WCP instance to prevent memory leak
wcp->finish_job();
}
}
void WCPDownloadManager::cleanup_task(size_t task_id) {
std::lock_guard<std::mutex> lock(m_tasks_mutex);
m_tasks.erase(task_id);
m_last_percent.erase(task_id);
m_last_update.erase(task_id);
}
}} // namespace Slic3r::GUI
-104
View File
@@ -1,104 +0,0 @@
#ifndef slic3r_WCPDownloadManager_hpp_
#define slic3r_WCPDownloadManager_hpp_
#include <memory>
#include <string>
#include <unordered_map>
#include <mutex>
#include <atomic>
#include <chrono>
#include "../Utils/Http.hpp"
#include "SSWCP.hpp"
#include <boost/filesystem/path.hpp>
#include "nlohmann/json.hpp"
namespace Slic3r { namespace GUI {
// Download task state
enum class WCPDownloadState {
Pending,
Downloading,
Paused,
Completed,
Error,
Canceled
};
// Download task information
struct WCPDownloadTask {
size_t task_id;
std::string file_url;
std::string file_name;
std::string dest_path;
std::weak_ptr<SSWCP_Instance> wcp_instance; // Associated WCP instance
Http::Ptr http_object; // HTTP object for cancellation
WCPDownloadState state;
int percent;
std::string error_message;
WCPDownloadTask(size_t id, const std::string& url, const std::string& name,
const std::string& path, std::shared_ptr<SSWCP_Instance> instance)
: task_id(id), file_url(url), file_name(name), dest_path(path),
wcp_instance(instance), state(WCPDownloadState::Pending), percent(0) {}
};
// WCP Download Manager
class WCPDownloadManager {
public:
static WCPDownloadManager& getInstance() {
static WCPDownloadManager instance;
return instance;
}
// Start a download task
size_t start_download(const std::string& file_url,
const std::string& file_name,
std::shared_ptr<SSWCP_Instance> wcp_instance);
// Cancel a download task
bool cancel_download(size_t task_id);
// Pause a download task (if needed)
bool pause_download(size_t task_id);
// Resume a download task (if needed)
bool resume_download(size_t task_id);
// Get task state
WCPDownloadState get_task_state(size_t task_id);
// Get task information
std::shared_ptr<WCPDownloadTask> get_task(size_t task_id);
private:
WCPDownloadManager() = default;
~WCPDownloadManager() = default;
WCPDownloadManager(const WCPDownloadManager&) = delete;
WCPDownloadManager& operator=(const WCPDownloadManager&) = delete;
std::mutex m_tasks_mutex;
std::unordered_map<size_t, std::shared_ptr<WCPDownloadTask>> m_tasks;
std::atomic<size_t> m_next_task_id{1};
// Track last progress update for throttling
std::unordered_map<size_t, int> m_last_percent;
std::unordered_map<size_t, std::chrono::steady_clock::time_point> m_last_update;
// Send progress update to WCP
void send_progress_update(std::shared_ptr<WCPDownloadTask> task, int percent,
size_t downloaded, size_t total);
// Send completion message to WCP
void send_complete_update(std::shared_ptr<WCPDownloadTask> task, const std::string& file_path);
// Send error message to WCP
void send_error_update(std::shared_ptr<WCPDownloadTask> task, const std::string& error);
// Clean up completed task
void cleanup_task(size_t task_id);
};
}} // namespace Slic3r::GUI
#endif // slic3r_WCPDownloadManager_hpp_
+35 -3
View File
@@ -96,6 +96,22 @@ void WebPreprintDialog::set_display_file_name(const std::string& filename) {
void WebPreprintDialog::set_gcode_file_name(const std::string& filename)
{ m_gcode_file_name = filename; }
void WebPreprintDialog::set_finish(bool flag)
{
m_finish = flag;
// BBS: Don't call EndModal here to avoid conflict with sw_FinishFilamentMapping()
// The external sw_FinishFilamentMapping() function will handle EndModal based on m_finish flag
}
void WebPreprintDialog::SafeEndModal(int returnCode)
{
// BBS: Prevent duplicate EndModal calls which can cause crashes
if (IsModal() && !m_modal_ended) {
m_modal_ended = true;
EndModal(returnCode);
}
}
void WebPreprintDialog::reload()
{
load_url(m_prePrint_url);
@@ -123,8 +139,16 @@ bool WebPreprintDialog::run()
}
this->load_url(real_url);
if (this->ShowModal() == wxID_OK) {
return true;
// BBS: Reset flags before showing modal
m_finish = false;
m_modal_ended = false;
int result = this->ShowModal();
// BBS: Check finish flag to determine return value
if (result == wxID_OK || (result == wxID_CANCEL && m_finish)) {
return m_finish;
}
return false;
}
@@ -186,7 +210,15 @@ void WebPreprintDialog::OnClose(wxCloseEvent& evt)
{
auto noti_manager = wxGetApp().mainframe->plater()->get_notification_manager();
noti_manager->close_notification_of_type(NotificationType::PrintHostUpload);
evt.Skip();
// BBS: Use SafeEndModal to prevent duplicate EndModal calls
// This ensures consistency with sw_FinishFilamentMapping() and prevents crashes
SafeEndModal(wxID_CANCEL);
// If not modal or already ended, skip the event
if (!IsModal() || m_modal_ended) {
evt.Skip();
}
}
}} // namespace Slic3r::GUI
+5 -1
View File
@@ -33,7 +33,10 @@ public:
bool is_finish() { return m_finish; }
void set_finish(bool flag) { m_finish = flag; }
void set_finish(bool flag);
// BBS: Safely end modal dialog, preventing duplicate EndModal calls
void SafeEndModal(int returnCode);
private:
void OnClose(wxCloseEvent& evt);
@@ -53,6 +56,7 @@ private:
bool m_switch_to_device = false;
bool m_finish = false;
bool m_modal_ended = false; // BBS: Flag to prevent duplicate EndModal calls
DECLARE_EVENT_TABLE()
};
+77 -73
View File
@@ -232,7 +232,11 @@ struct PresetUpdater::priv
void sync_resources(std::string http_url, std::map<std::string, Resource> &resources, bool check_patch = false, std::string current_version="", std::string changelog_file="");
void sync_config(bool isAuto_check = true);
void sync_update_flutter_resource(bool isAuto_check = true);
bool download_file(const std::string& url, const std::string& target_path, int timeout_sec = 30, bool* cancel_flag = nullptr);
bool download_file(const std::string& url,
const std::string& target_path,
const std::string& extract_path,
int timeout_sec = 30,
bool* cancel_flag = nullptr);
void sync_tooltip(std::string http_url, std::string language);
void sync_plugins(std::string http_url, std::string plugin_version);
void sync_printer_config(std::string http_url);
@@ -320,7 +324,7 @@ bool PresetUpdater::priv::extract_file(const fs::path &source_path, const fs::pa
{
bool res = true;
std::string file_path = source_path.string();
std::string parent_path = (!dest_path.empty() ? dest_path : source_path.parent_path()).string();
fs::path parent_path = !dest_path.empty() ? dest_path : source_path.parent_path();
mz_zip_archive archive;
mz_zip_zero_struct(&archive);
@@ -331,6 +335,7 @@ bool PresetUpdater::priv::extract_file(const fs::path &source_path, const fs::pa
}
mz_uint num_entries = mz_zip_reader_get_num_files(&archive);
fs::path base_path = parent_path.lexically_normal();
mz_zip_archive_file_stat stat;
// we first loop the entries to read from the archive the .amf file only, in order to extract the version from it
@@ -338,30 +343,48 @@ bool PresetUpdater::priv::extract_file(const fs::path &source_path, const fs::pa
{
if (mz_zip_reader_file_stat(&archive, i, &stat))
{
std::string dest_file = parent_path+"/"+stat.m_filename;
if (stat.m_is_directory) {
fs::path dest_path(dest_file);
if (!fs::exists(dest_path))
fs::create_directories(dest_path);
continue;
fs::path full_dest = (base_path / stat.m_filename).lexically_normal();
// Reject paths that escape base (e.g. ".." in zip entry)
std::string rel_str = full_dest.lexically_relative(base_path).generic_string();
if (rel_str.empty() || rel_str.find("..") == 0) {
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater]Unzip: skip invalid path "<<stat.m_filename;
continue;
}
else if (stat.m_uncomp_size == 0) {
if (stat.m_is_directory) {
if (!fs::exists(full_dest))
fs::create_directories(full_dest);
continue;
}
if (stat.m_uncomp_size == 0) {
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater]Unzip: invalid size for file "<<stat.m_filename;
continue;
}
try
{
res = mz_zip_reader_extract_to_file(&archive, stat.m_file_index, dest_file.c_str(), 0);
// Ensure parent directory exists (zip often has no directory entries, e.g. "flutter_web/version.json" only)
fs::path parent_dir = full_dest.parent_path();
if (!parent_dir.empty() && !fs::exists(parent_dir))
fs::create_directories(parent_dir);
std::string dest_file_encoded = encode_path(full_dest.string().c_str());
res = mz_zip_reader_extract_to_file(&archive, stat.m_file_index, dest_file_encoded.c_str(), 0);
#ifdef _WIN32
if (!res) {
BOOST_LOG_TRIVIAL(error) << "[Orca Updater]extract file "<<stat.m_filename<<" to dest "<<dest_file<<" failed";
close_zip_reader(&archive);
return res;
std::wstring dest_file_w = boost::nowide::widen(full_dest.generic_string());
res = mz_zip_reader_extract_to_file_w(&archive, stat.m_file_index, dest_file_w.c_str(), 0);
}
BOOST_LOG_TRIVIAL(info) << "[Orca Updater]successfully extract file " << stat.m_file_index << " to "<<dest_file;
#endif
if (!res) {
mz_zip_error zip_err = mz_zip_get_last_error(&archive);
BOOST_LOG_TRIVIAL(error) << "[Orca Updater]extract file "<<stat.m_filename<<" to dest "<<full_dest.string()
<< " failed: " << (zip_err != MZ_ZIP_NO_ERROR ? mz_zip_get_error_string(zip_err) : "unknown");
close_zip_reader(&archive);
return false;
}
BOOST_LOG_TRIVIAL(info) << "[Orca Updater]successfully extract file " << stat.m_file_index << " to "<<full_dest.string();
}
catch (const std::exception& e)
{
// ensure the zip archive is closed and rethrow the exception
close_zip_reader(&archive);
BOOST_LOG_TRIVIAL(error) << "[Orca Updater]Archive read exception:"<<e.what();
return false;
@@ -373,7 +396,7 @@ bool PresetUpdater::priv::extract_file(const fs::path &source_path, const fs::pa
}
close_zip_reader(&archive);
return true;
return true;
}
// Remove leftover paritally downloaded files, if any.
@@ -657,6 +680,7 @@ void PresetUpdater::priv::sync_resources(std::string http_url, std::map<std::str
}
bool PresetUpdater::priv::download_file(const std::string& url,
const std::string& target_path,
const std::string& extract_path,
int timeout_sec,
bool* cancel_flag )
{
@@ -676,7 +700,7 @@ bool PresetUpdater::priv::download_file(const std::string& url,
.on_error([&url](std::string body, std::string error, unsigned http_status) {
BOOST_LOG_TRIVIAL(error) << "Download failed: " << url << ", HTTP status: " << http_status << ", error: " << error;
})
.on_complete([&](std::string body, unsigned http_status) {
.on_complete([&, target_path,tmp_path,extract_path](std::string body, unsigned http_status) {
if (http_status != 200) {
BOOST_LOG_TRIVIAL(error) << "Download failed with HTTP status: " << http_status;
return;
@@ -700,12 +724,12 @@ bool PresetUpdater::priv::download_file(const std::string& url,
BOOST_LOG_TRIVIAL(error) << "Failed to rename temp file: " << ec.message();
return;
}
extract_file(target_path, "../ota/profiles/");
extract_file(target_path, extract_path);
BOOST_LOG_TRIVIAL(info) << "Download completed: " << target_path;
res = true;
})
.timeout_max(timeout_sec)
.perform_sync();
.perform();
if (fs::exists(tmp_path)) {
fs::remove(tmp_path);
@@ -803,8 +827,19 @@ void PresetUpdater::priv::sync_update_flutter_resource(bool isAuto_check)
return;
}
if (currentPresetVersion < remoteVersion)
download_file(fileUrl, fileName);
if (currentPresetVersion < remoteVersion) {
if (fs::exists(fileName))
fs::remove(fileName);
fs::path tmpPath = fileName;
auto dirPath = tmpPath.parent_path() / "profiles/flutter_web";
if (fs::exists(dirPath))
fs::remove_all(dirPath);
download_file(fileUrl, fileName, "../ota/profiles/");
}
else {
if (!isAuto_check) {
wxCommandEvent* evt = new wxCommandEvent(EVT_NO_WEB_RESOURCE_UPDATE);
@@ -819,7 +854,7 @@ void PresetUpdater::priv::sync_update_flutter_resource(bool isAuto_check)
BOOST_LOG_TRIVIAL(fatal) << "request server flutter update data error:" << errorMsg;
}
})
.perform_sync();
.perform();
}
// Orca: sync config update for currect App version
void PresetUpdater::priv::sync_config(bool isAuto_check)
@@ -912,8 +947,18 @@ void PresetUpdater::priv::sync_config(bool isAuto_check)
return;
}
if (currentPresetVersion < remoteVersion)
download_file(fileUrl, fileName);
if (currentPresetVersion < remoteVersion) {
if (fs::exists(fileName))
fs::remove(fileName);
fs::path tmpPath = fileName;
auto dirPath = tmpPath.parent_path() / "profiles/profiles";
if (fs::exists(dirPath))
fs::remove_all(dirPath);
download_file(fileUrl, fileName, "../ota/profiles/profiles/");
}
else {
if (!isAuto_check) {
wxCommandEvent* evt = new wxCommandEvent(EVT_NO_PRESET_UPDATE);
@@ -928,7 +973,7 @@ void PresetUpdater::priv::sync_config(bool isAuto_check)
BOOST_LOG_TRIVIAL(fatal) << "request server preset update data error:" << errorMsg;
}
})
.perform_sync();
.perform();
}
void PresetUpdater::priv::sync_tooltip(std::string http_url, std::string language)
@@ -1375,7 +1420,7 @@ Updates PresetUpdater::priv::get_config_updates(const Semver &old_slic3r_version
Updates updates;
BOOST_LOG_TRIVIAL(info) << "[Orca Updater]:Checking for cached configuration updates...";
auto cache_profile_path = cache_path / "profiles";
auto cache_profile_path = cache_path / "profiles/profiles";
if (!fs::exists(cache_profile_path))
return updates;
@@ -1465,15 +1510,8 @@ Updates PresetUpdater::priv::get_config_updates(const Semver &old_slic3r_version
//BBS: switch to new BBL.json configs
bool PresetUpdater::priv::perform_updates(Updates &&updates, bool snapshot) const
{
//std::string vendor_path;
//std::string vendor_name;
if (updates.incompats.size() > 0) {
//if (snapshot) {
// BOOST_LOG_TRIVIAL(info) << "Taking a snapshot...";
// if (! GUI::Config::take_config_snapshot_cancel_on_error(*GUI::wxGetApp().app_config, Snapshot::SNAPSHOT_DOWNGRADE, "",
// _u8L("Continue and install configuration updates?")))
// return false;
//}
BOOST_LOG_TRIVIAL(info) << format("[Orca Updater]:Deleting %1% incompatible bundles", updates.incompats.size());
for (auto &incompat : updates.incompats) {
@@ -1481,12 +1519,6 @@ bool PresetUpdater::priv::perform_updates(Updates &&updates, bool snapshot) cons
incompat.remove();
}
} else if (updates.updates.size() > 0) {
//if (snapshot) {
// BOOST_LOG_TRIVIAL(info) << "Taking a snapshot...";
// if (! GUI::Config::take_config_snapshot_cancel_on_error(*GUI::wxGetApp().app_config, Snapshot::SNAPSHOT_UPGRADE, "",
// _u8L("Continue and install configuration updates?")))
// return false;
//}
BOOST_LOG_TRIVIAL(info) << format("[Orca Updater]:Performing %1% updates", updates.updates.size());
@@ -1495,28 +1527,8 @@ bool PresetUpdater::priv::perform_updates(Updates &&updates, bool snapshot) cons
if (update.can_install)
update.install();
//if (!update.is_directory) {
// vendor_path = update.source.parent_path().string();
// vendor_name = update.vendor;
//}
}
//if (!vendor_path.empty()) {
// PresetBundle bundle;
// // Throw when parsing invalid configuration. Only valid configuration is supposed to be provided over the air.
// bundle.load_vendor_configs_from_json(vendor_path, vendor_name, PresetBundle::LoadConfigBundleAttribute::LoadSystem, ForwardCompatibilitySubstitutionRule::Disable);
// BOOST_LOG_TRIVIAL(info) << format("Deleting %1% conflicting presets", bundle.prints.size() + bundle.filaments.size() + bundle.printers.size());
// auto preset_remover = [](const Preset& preset) {
// BOOST_LOG_TRIVIAL(info) << '\t' << preset.file;
// fs::remove(preset.file);
// };
// for (const auto &preset : bundle.prints) { preset_remover(preset); }
// for (const auto &preset : bundle.filaments) { preset_remover(preset); }
// for (const auto &preset : bundle.printers) { preset_remover(preset); }
//}
}
return true;
@@ -1555,12 +1567,9 @@ PresetUpdater::~PresetUpdater()
//BBS: refine the preset updater logic
void PresetUpdater::sync(std::string http_url, std::string language, std::string plugin_version, PresetBundle *preset_bundle)
{
//p->set_download_prefs(GUI::wxGetApp().app_config);
if (!p->enabled_version_check && !p->enabled_config_update) { return; }
// Copy the whole vendors data for use in the background thread
// Unfortunatelly as of C++11, it needs to be copied again
// into the closure (but perhaps the compiler can elide this).
VendorMap vendors = preset_bundle ? preset_bundle->vendors : VendorMap{};
p->thread = std::thread([this, vendors, http_url, language, plugin_version]() {
@@ -1582,10 +1591,7 @@ void PresetUpdater::sync(std::string http_url, std::string language, std::string
return;
this->p->sync_plugins(http_url, plugin_version);
this->p->sync_printer_config(http_url);
//if (p->cancel)
// return;
//remove the tooltip currently
//this->p->sync_tooltip(http_url, language);
});
}
@@ -1603,9 +1609,7 @@ static bool reload_configs_update_gui()
// Reload global configuration
auto* app_config = GUI::wxGetApp().app_config;
// System profiles should not trigger any substitutions, user profiles may trigger substitutions, but these substitutions
// were already presented to the user on application start up. Just do substitutions now and keep quiet about it.
// However throw on substitutions in system profiles, those shall never happen with system profiles installed over the air.
GUI::wxGetApp().preset_bundle->load_presets(*app_config, ForwardCompatibilitySubstitutionRule::EnableSilentDisableSystem);
GUI::wxGetApp().load_current_presets();
GUI::wxGetApp().plater()->set_bed_shape();
+200
View File
@@ -0,0 +1,200 @@
# 参数访问修复表
## 修复概述
**问题**: 在8耗材4挤出机的配置下,使用耗材ID(0-7)访问只有4个元素的物理挤出机参数数组,导致数组越界。
**解决**: 通过 `get_physical_extruder(filament_id)` 获取正确的物理挤出机ID进行访问。
**修复统计**:
- 修改文件: 9个
- 修改代码行: 约40处
- 修复参数: 22个
---
## GCode.cpp 中的修复
| 代码位置 | 参数名 | 原来索引 | 现在索引 | 有无改动 | 改动原因 |
|---------|-------|---------|---------|---------|---------|
| 1051-1054 | EXTRUDER_CONFIG宏定义 | `m_writer.extruder()->id()` (filament_id) | `m_writer.extruder()->id()` (filament_id) | ❌ 无 | 用于耗材参数,保持不变 |
| 1053-1054 | **PHYSICAL_EXTRUDER_CONFIG宏定义** | - | `m_writer.get_physical_extruder(m_writer.extruder()->id())` | ✅ **新增** | **新增宏用于访问物理挤出机参数** |
| 2857-2858 | nozzle_diameter (spiral_vase) | `extruder()->id()` (filament_id) | `get_physical_extruder(extruder()->id())` | ✅ **已改** | nozzle_diameter是物理挤出机参数,只有4个元素,filament 5-7会越界 |
| 2958-2959 | nozzle_diameter (spiral_vase) | `extruder()->id()` (filament_id) | `get_physical_extruder(extruder()->id())` | ✅ **已改** | 同上 |
| 4771-4772 | nozzle_diameter (seam_slope) | `extruder()->id()` (filament_id) | `get_physical_extruder(extruder()->id())` | ✅ **已改** | 同上 |
| 4995-4996 | nozzle_diameter (hide_seam) | `extruder()->id()` (filament_id) | `get_physical_extruder(extruder()->id())` | ✅ **已改** | 同上 |
| 5589-5590 | enable_pressure_advance | `extruder()->id()` (filament_id) | `get_physical_extruder(extruder()->id())` | ✅ **已改** | 无耗材覆盖,物理挤出机参数 |
| 5589-5590 | adaptive_pressure_advance | `extruder()->id()` (filament_id) | `get_physical_extruder(extruder()->id())` | ✅ **已改** | 无耗材覆盖,物理挤出机参数 |
| 5785-5789 | enable_pressure_advance | `extruder()->id()` (filament_id) | `get_physical_extruder(extruder()->id())` | ✅ **已改** | 无耗材覆盖,物理挤出机参数 |
| 5785-5789 | adaptive_pressure_advance | `extruder()->id()` (filament_id) | `get_physical_extruder(extruder()->id())` | ✅ **已改** | 无耗材覆盖,物理挤出机参数 |
| 5785-5789 | adaptive_pressure_advance_overhangs | `extruder()->id()` (filament_id) | `get_physical_extruder(extruder()->id())` | ✅ **已改** | 无耗材覆盖,物理挤出机参数 |
| 5995-5999 | enable_pressure_advance | `extruder()->id()` (filament_id) | `get_physical_extruder(extruder()->id())` | ✅ **已改** | 无耗材覆盖,物理挤出机参数 |
| 5995-5999 | adaptive_pressure_advance | `extruder()->id()` (filament_id) | `get_physical_extruder(extruder()->id())` | ✅ **已改** | 无耗材覆盖,物理挤出机参数 |
| 5995-5999 | adaptive_pressure_advance_overhangs | `extruder()->id()` (filament_id) | `get_physical_extruder(extruder()->id())` | ✅ **已改** | 无耗材覆盖,物理挤出机参数 |
| 5702-5703 | overhang_fan_threshold | `extruder()->id()` (filament_id) | `get_physical_extruder(extruder()->id())` | ✅ **已改** | 无耗材覆盖,物理挤出机参数 |
| 5702-5703 | enable_overhang_bridge_fan | `extruder()->id()` (filament_id) | `get_physical_extruder(extruder()->id())` | ✅ **已改** | 无耗材覆盖,物理挤出机参数 |
| 5769-5771 | support_material_interface_fan_speed | `extruder()->id()` (filament_id) | `get_physical_extruder(extruder()->id())` | ✅ **已改** | 无耗材覆盖,物理挤出机参数 |
| 5769-5771 | ironing_fan_speed | `extruder()->id()` (filament_id) | `get_physical_extruder(extruder()->id())` | ✅ **已改** | 无耗材覆盖,物理挤出机参数 |
| 6510-6515 | enable_pressure_advance (set_extruder) | `extruder_id` (filament_id) | `physical_extruder_id` | ✅ **已改** | 无耗材覆盖,物理挤出机参数 |
| 6510-6515 | pressure_advance (set_extruder) | `extruder_id` (filament_id) | `physical_extruder_id` | ✅ **已改** | 无耗材覆盖,物理挤出机参数 |
| 6760-6763 | enable_pressure_advance (set_extruder) | `extruder_id` (filament_id) | `physical_extruder_id` | ✅ **已改** | 无耗材覆盖,物理挤出机参数 |
| 6760-6763 | pressure_advance (set_extruder) | `extruder_id` (filament_id) | `physical_extruder_id` | ✅ **已改** | 无耗材覆盖,物理挤出机参数 |
| 5437, 5475, 5491, 5517, 5519 | filament_max_volumetric_speed | `extruder()->id()` (filament_id) | `extruder()->id()` (filament_id) | ❌ 无 | 耗材参数(名称带filament_),使用耗材索引正确 |
| 4685-4688 | retract_when_changing_layer | `extruder()->id()` (filament_id) | `extruder()->id()` (filament_id) | ❌ 无 | 有耗材覆盖(filament_retract_when_changing_layer) |
| 4685-4688 | z_hop_types | `extruder()->id()` (filament_id) | `extruder()->id()` (filament_id) | ❌ 无 | 有耗材覆盖(filament_z_hop_types) |
| 6286 | retraction_minimum_travel | `extruder()->id()` (filament_id) | `extruder()->id()` (filament_id) | ❌ 无 | 有耗材覆盖 |
| 6365-6369 | z_hop_types | `extruder()->id()` (filament_id) | `extruder()->id()` (filament_id) | ❌ 无 | 有耗材覆盖 |
| 6399-6403 | z_hop_types | `extruder()->id()` (filament_id) | `extruder()->id()` (filament_id) | ❌ 无 | 有耗材覆盖 |
| 6416 | wipe | `extruder()->id()` (filament_id) | `extruder()->id()` (filament_id) | ❌ 无 | 有耗材覆盖 |
| 6416 | wipe_distance | `extruder()->id()` (filament_id) | `extruder()->id()` (filament_id) | ❌ 无 | 有耗材覆盖 |
| 6440 | retract_lift_enforce | `extruder()->id()` (filament_id) | `extruder()->id()` (filament_id) | ❌ 无 | 有耗材覆盖 |
---
## CoolingBuffer.cpp 中的修复
| 代码位置 | 参数名 | 原来索引 | 现在索引 | 有无改动 | 改动原因 |
|---------|-------|---------|---------|---------|---------|
| 734-735 | EXTRUDER_CONFIG宏定义 | `m_current_extruder` (filament_id) | `get_physical_extruder(m_current_extruder)` | ✅ **已改** | **所有风扇参数都是物理挤出机参数** |
| 736 | fan_min_speed | `m_current_extruder` | `get_physical_extruder(m_current_extruder)` | ✅ **已改** | 无耗材覆盖,物理挤出机参数 |
| 737 | reduce_fan_stop_start_freq | `m_current_extruder` | `get_physical_extruder(m_current_extruder)` | ✅ **已改** | 无耗材覆盖,物理挤出机参数 |
| 739 | additional_cooling_fan_speed | `m_current_extruder` | `get_physical_extruder(m_current_extruder)` | ✅ **已改** | 无耗材覆盖,物理挤出机参数 |
| 740 | close_fan_the_first_x_layers | `m_current_extruder` | `get_physical_extruder(m_current_extruder)` | ✅ **已改** | 无耗材覆盖,物理挤出机参数 |
| 742 | full_fan_speed_layer | `m_current_extruder` | `get_physical_extruder(m_current_extruder)` | ✅ **已改** | 无耗材覆盖,物理挤出机参数 |
| 743 | support_material_interface_fan_speed | `m_current_extruder` | `get_physical_extruder(m_current_extruder)` | ✅ **已改** | 无耗材覆盖,物理挤出机参数 |
| 751 | fan_max_speed | `m_current_extruder` | `get_physical_extruder(m_current_extruder)` | ✅ **已改** | 无耗材覆盖,物理挤出机参数 |
| 752 | slow_down_layer_time | `m_current_extruder` | `get_physical_extruder(m_current_extruder)` | ✅ **已改** | 无耗材覆盖,物理挤出机参数 |
| 753 | fan_cooling_layer_time | `m_current_extruder` | `get_physical_extruder(m_current_extruder)` | ✅ **已改** | 无耗材覆盖,物理挤出机参数 |
| 766 | overhang_fan_speed | `m_current_extruder` | `get_physical_extruder(m_current_extruder)` | ✅ **已改** | 无耗材覆盖,物理挤出机参数 |
| 773 | support_material_interface_fan_speed | `m_current_extruder` | `get_physical_extruder(m_current_extruder)` | ✅ **已改** | 无耗材覆盖,物理挤出机参数 |
| 779 | internal_bridge_fan_speed | `m_current_extruder` | `get_physical_extruder(m_current_extruder)` | ✅ **已改** | 无耗材覆盖,物理挤出机参数 |
| 788 | ironing_fan_speed | `m_current_extruder` | `get_physical_extruder(m_current_extruder)` | ✅ **已改** | 无耗材覆盖,物理挤出机参数 |
| 344-350 | slow_down_for_layer_cooling | `extruder_id` | `get_physical_extruder(extruder_id)` | ✅ **已改** | 无耗材覆盖,物理挤出机参数 |
| 347 | slow_down_layer_time | `extruder_id` | `get_physical_extruder(extruder_id)` | ✅ **已改** | 无耗材覆盖,物理挤出机参数 |
| 348 | slow_down_min_speed | `extruder_id` | `get_physical_extruder(extruder_id)` | ✅ **已改** | 无耗材覆盖,物理挤出机参数 |
| 350 | dont_slow_down_outer_wall | `extruder_id` | `get_physical_extruder(extruder_id)` | ✅ **已改** | 无耗材覆盖,物理挤出机参数 |
---
## AvoidCrossingPerimeters.cpp 中的修复
| 代码位置 | 参数名 | 原来索引 | 现在索引 | 有无改动 | 改动原因 |
|---------|-------|---------|---------|---------|---------|
| 486-488 | nozzle_diameter | `extruder_id` (filament_id) | `get_physical_extruder(extruder_id)` | ✅ **已改** | nozzle_diameter是物理挤出机参数,只有4个元素 |
---
## Extruder.cpp 中的修复(之前已完成)
| 代码位置 | 参数名 | 原来索引 | 现在索引 | 有无改动 | 改动原因 |
|---------|-------|---------|---------|---------|---------|
| 163 | retract_before_wipe | `m_physical_extruder_id` | `m_id` | ✅ **已改** | 有耗材覆盖,继承后存储在耗材位置 |
| 168 | retraction_length | `m_physical_extruder_id` | `m_id` | ✅ **已改** | 有耗材覆盖 |
| 173 | z_hop | `m_physical_extruder_id` | `m_id` | ✅ **已改** | 有耗材覆盖 |
| 178 | retraction_speed | `m_physical_extruder_id` | `m_id` | ✅ **已改** | 有耗材覆盖 |
| 188 | deretraction_speed | `m_physical_extruder_id` | `m_id` | ✅ **已改** | 有耗材覆盖 |
| 194 | retract_restart_extra | `m_physical_extruder_id` | `m_id` | ✅ **已改** | 有耗材覆盖 |
| 199 | retract_length_toolchange | `m_physical_extruder_id` | `m_id` | ✅ **已改** | 有耗材覆盖 |
| 204 | retract_restart_extra_toolchange | `m_physical_extruder_id` | `m_id` | ✅ **已改** | 有耗材覆盖 |
| 209 | travel_slope | `m_physical_extruder_id` | `m_id` | ✅ **已改** | 有耗材覆盖 |
---
## GCode.cpp 中未改动(正确的耗材参数)
| 代码位置 | 参数名 | 使用索引 | 是否改动 | 原因 |
|---------|-------|---------|---------|------|
| 262, 274 | filament_idle_temp | `extruder_id` | ❌ 无 | 耗材参数(名称带filament_) |
| 333 | wipe_distance | `extruder_id` | ❌ 无 | 有耗材覆盖 |
| 339 | retraction_speed | `extruder_id` | ❌ 无 | 有耗材覆盖 |
| 2731 | filament_end_gcode | `extruder_id` | ❌ 无 | 耗材参数 |
| 3979, 4044, 4074 | filament_soluble | `extruder_id` | ❌ 无 | 耗材参数 |
| 6494 | filament_start_gcode | `extruder_id` | ❌ 无 | 耗材参数 |
| 6503 | retraction_distances_when_cut | `extruder_id` | ❌ 无 | 有耗材覆盖 |
| 6504 | long_retractions_when_cut | `extruder_id` | ❌ 无 | 有耗材覆盖 |
| 6581 | retraction_length | `extruder_id` | ❌ 无 | 有耗材覆盖 |
| 6582 | retract_length_toolchange | `extruder_id` | ❌ 无 | 有耗材覆盖 |
| 6584, 6587, 6734-6735 | nozzle_temperature | `extruder_id` | ❌ 无 | 耗材参数(在耗材配置文件中定义) |
| 6593 | filament_diameter | `extruder_id` | ❌ 无 | 耗材参数 |
| 6632 | filament_max_volumetric_speed | `extruder_id` | ❌ 无 | 耗材参数 |
| 6741-6742 | retraction_distances_when_cut | `extruder_id` | ❌ 无 | 有耗材覆盖 |
---
## 修复的物理挤出机参数列表(共22个)
| 参数 | 说明 | 数组大小 | 修复文件 |
|------|------|----------|----------|
| nozzle_diameter | 喷嘴直径 | physical_extruder_count (4) | GCode.cpp, AvoidCrossingPerimeters.cpp |
| enable_pressure_advance | 启用压力提前 | physical_extruder_count (4) | GCode.cpp |
| adaptive_pressure_advance | 自适应压力提前 | physical_extruder_count (4) | GCode.cpp |
| adaptive_pressure_advance_overhangs | 悬空自适应PA | physical_extruder_count (4) | GCode.cpp |
| pressure_advance | 压力提前值 | physical_extruder_count (4) | GCode.cpp |
| overhang_fan_threshold | 悬空风扇阈值 | physical_extruder_count (4) | GCode.cpp |
| enable_overhang_bridge_fan | 悬空桥风扇开关 | physical_extruder_count (4) | GCode.cpp |
| overhang_fan_speed | 悬空风扇速度 | physical_extruder_count (4) | CoolingBuffer.cpp |
| support_material_interface_fan_speed | 支持界面风扇速度 | physical_extruder_count (4) | GCode.cpp, CoolingBuffer.cpp |
| ironing_fan_speed | 熨烫风扇速度 | physical_extruder_count (4) | GCode.cpp, CoolingBuffer.cpp |
| internal_bridge_fan_speed | 内部桥风扇速度 | physical_extruder_count (4) | CoolingBuffer.cpp |
| fan_min_speed | 最小风扇速度 | physical_extruder_count (4) | CoolingBuffer.cpp |
| fan_max_speed | 最大风扇速度 | physical_extruder_count (4) | CoolingBuffer.cpp |
| slow_down_layer_time | 减速层时间 | physical_extruder_count (4) | CoolingBuffer.cpp |
| slow_down_for_layer_cooling | 层冷却减速开关 | physical_extruder_count (4) | CoolingBuffer.cpp |
| slow_down_min_speed | 最小减速速度 | physical_extruder_count (4) | CoolingBuffer.cpp |
| fan_cooling_layer_time | 风扇冷却层时间 | physical_extruder_count (4) | CoolingBuffer.cpp |
| close_fan_the_first_x_layers | 前N层关闭风扇 | physical_extruder_count (4) | CoolingBuffer.cpp |
| full_fan_speed_layer | 全速风扇层 | physical_extruder_count (4) | CoolingBuffer.cpp |
| additional_cooling_fan_speed | 额外冷却风扇速度 | physical_extruder_count (4) | CoolingBuffer.cpp |
| reduce_fan_stop_start_frequency | 减少风扇启停频率 | physical_extruder_count (4) | CoolingBuffer.cpp |
| dont_slow_down_outer_wall | 外墙不减速 | physical_extruder_count (4) | CoolingBuffer.cpp |
---
## 修复统计汇总
| 类别 | 数量 |
|------|------|
| **需要改动的参数** | **22个** |
| **修改的代码行数** | **约40处** |
| **新增的宏** | **1个 (PHYSICAL_EXTRUDER_CONFIG)** |
| **新增的方法** | **2个 (CoolingBuffer)** |
---
## 核心问题与解决方案
### 问题根源
```
8耗材 (索引0-7) → 访问4元素数组 → 索引5-7时数组越界 ❌
```
### 解决方案
```
耗材ID → 映射表 → 物理挤出机ID (0-3) → 访问4元素数组 ✅
```
### 关键改动
1. **新增 `PHYSICAL_EXTRUDER_CONFIG` 宏**:专门用于访问物理挤出机参数
```cpp
#define PHYSICAL_EXTRUDER_CONFIG(OPT) m_config.OPT.get_at(m_writer.get_physical_extruder(m_writer.extruder()->id()))
```
2. **CoolingBuffer 添加映射表和方法**
- `m_filament_extruder_map`:存储耗材到物理挤出机的映射
- `get_physical_extruder(filament_idx)`:获取物理挤出机ID
- `set_filament_extruder_map(map)`:设置映射表
3. **所有物理挤出机参数改用 `physical_extruder_id`**
- 原来使用 `filament_id` (0-7)
- 现在使用 `physical_extruder_id` (0-3)
- 确保不会数组越界
---
## 验证检查点
- ✅ GCode.cpp 中的 PHYSICAL_EXTRUDER_CONFIG 宏正确使用
- ✅ CoolingBuffer 中的映射表正确设置
- ✅ 所有物理挤出机参数使用 physical_extruder_id 访问
- ✅ 所有耗材参数使用 filament_id 访问
- ✅ 大于4号的耗材能够正确切片,不再越界
+127
View File
@@ -0,0 +1,127 @@
# 耗材继承修复总结
**状态**: ✅ 已完成并验证工作
## 问题描述
### 问题1: 打印机配置参数无法修改
修改挤出机2的回抽长度从1.5到1.6后,值会闪回1.5
### 问题2: 所有耗材都从挤出机1继承参数
期望:
- 耗材1 → 继承挤出机1
- 耗材2 → 继承挤出机2
- 耗材3 → 继承挤出机3
- 耗材4 → 继承挤出机4
- 耗材5 → 继承挤出机1(通过 filament_extruder_map 映射)
- 耗材6 → 继承挤出机2(通过 filament_extruder_map 映射)
实际:所有耗材都继承挤出机1的参数
## 修复方案
### 修复1: GUI.cpp (打印机配置修改)
**文件**: `src/slic3r/GUI/GUI.cpp`
**位置**: 第139行、第144行
```cpp
// 之前(错误):
config.option<ConfigOptionPercents>(opt_key)->set_at(vec_new, opt_index, opt_index);
config.option<ConfigOptionFloats>(opt_key)->set_at(vec_new, opt_index, opt_index);
// 之后(正确):
config.option<ConfigOptionPercents>(opt_key)->set_at(vec_new, opt_index, 0); // SM Orca: Fix
config.option<ConfigOptionFloats>(opt_key)->set_at(vec_new, opt_index, 0); // SM Orca: Fix
```
**原因**: 用户输入单个值时创建的是1元素向量 `{value}`,原代码试图访问 `vec_new[opt_index]` 会越界
### 修复2: Tab.cpp (耗材继承)
**文件**: `src/slic3r/GUI/Tab.cpp`
#### 更改2.1: 获取实际耗材槽索引
**位置**: 第3317行、第3383行
```cpp
// 之前(硬编码):
const int extruder_idx = 0; // #ys_FIXME
// 之后(动态获取):
const int extruder_idx = (m_presets_choice && m_presets_choice->get_filament_idx() >= 0)
? m_presets_choice->get_filament_idx() : 0;
```
#### 更改2.2: 添加耗材→挤出机映射逻辑
**位置**: 第3289-3303行、第3418-3435行
```cpp
// SM Orca: Map filament slot to physical extruder index for inheritance
auto& filament_extruder_map = wxGetApp().app_config->get_filament_extruder_map_ref();
int physical_extruder_idx = opt_index; // default: filament N uses extruder N
auto map_it = filament_extruder_map.find(opt_index);
if (map_it != filament_extruder_map.end()) {
physical_extruder_idx = map_it->second;
}
// SM Orca: Bounds check to prevent crash from misconfigured map
const ConfigOptionFloats* nozzle_diameter = printer_config.option<ConfigOptionFloats>("nozzle_diameter");
int num_extruders = nozzle_diameter ? (int)nozzle_diameter->values.size() : 1;
if (physical_extruder_idx < 0 || physical_extruder_idx >= num_extruders) {
BOOST_LOG_TRIVIAL(warning) << "Invalid physical_extruder_idx " << physical_extruder_idx
<< " for filament slot " << opt_index << ", using default";
physical_extruder_idx = std::clamp(physical_extruder_idx, 0, num_extruders - 1);
}
const boost::any printer_config_value = optgroup_sh->get_config_value(printer_config, printer_opt_key, physical_extruder_idx);
```
## 架构验证
由 Architect (Opus) 验证:
- ✅ 架构正确
- ✅ 使用现有 filament_extruder_map 机制(只读,不修改)
- ✅ 逻辑流程正确
- ✅ 边界检查已添加
## 风险审视
### 低风险
1. **只读映射**: 代码只读取 `filament_extruder_map`,不修改映射机制本身
2. **边界检查**: 添加了边界检查,防止配置错误导致崩溃
3. **默认回退**: 如果映射不存在或索引无效,回退到 1:1 映射
### 需要注意的点
1. **filament_extruder_map 同步**:
- GUI 层使用 `AppConfig::filament_extruder_map`
- Print 层使用 `Print::m_filament_extruder_map`
- 需确保这两个映射保持同步
2. **多线程访问**:
- `get_filament_extruder_map_ref()` 返回非const引用
- 如果GUI和Print线程同时访问可能存在竞态
- 当前 AppConfig 似乎是主线程专用
## 保存当前状态
创建备份命令:
```bash
# 保存当前更改到stash
git stash save "耗材继承修复 - 工作版本"
# 或创建补丁文件
git diff src/slic3r/GUI/Tab.cpp > filament_inheritance_fix.patch
git diff src/slic3r/GUI/GUI.cpp > printer_config_fix.patch
```
## 修改的文件列表
1. `src/slic3r/GUI/GUI.cpp` - 打印机配置修改修复
2. `src/slic3r/GUI/Tab.cpp` - 耗材继承修复(4处更改)
3. `src/libslic3r/Config.hpp` - 添加了 set_at 重载方法
4. `src/libslic3r/PrintApply.cpp` - 相关调整
5. 其他GCode相关文件的适配性修改
**总计**: 17个文件,479行新增,135行删除