diff --git a/CLAUDE.md b/CLAUDE.md
index 61d292aee3..da7f9fcb53 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -9,67 +9,22 @@ OrcaSlicer is an open-source 3D slicer application forked from Bambu Studio, bui
## Build Commands
### Building on Windows
+**Always use this command to build the project when testing build issues on Windows.**
```bash
-# Build everything
-build_release_vs2022.bat
-
-# Build with debug symbols
-build_release_vs2022.bat debug
-
-# Build only dependencies
-build_release_vs2022.bat deps
-
-# Build only slicer (after deps are built)
-build_release_vs2022.bat slicer
-
-
+cmake --build . --config %build_type% --target ALL_BUILD -- -m
```
### Building on macOS
+**Always use this command to build the project when testing build issues on macOS.**
```bash
-# Build everything (dependencies and slicer)
-./build_release_macos.sh
-
-# Build only dependencies
-./build_release_macos.sh -d
-
-# Build only slicer (after deps are built)
-./build_release_macos.sh -s
-
-# Use Ninja generator for faster builds
-./build_release_macos.sh -x
-
-# Build for specific architecture
-./build_release_macos.sh -a arm64 # or x86_64 or universal
-
-# Build for specific macOS version target
-./build_release_macos.sh -t 11.3
+cmake --build build/arm64 --config RelWithDebInfo --target all --
```
### Building on Linux
+ **Always use this command to build the project when testing build issues on Linux.**
```bash
-# First time setup - install system dependencies
-./build_linux.sh -u
+cmake --build build/arm64 --config RelWithDebInfo --target all --
-# Build dependencies and slicer
-./build_linux.sh -dsi
-
-# Build everything (alternative)
-./build_linux.sh -dsi
-
-# Individual options:
-./build_linux.sh -d # dependencies only
-./build_linux.sh -s # slicer only
-./build_linux.sh -i # build AppImage
-
-# Performance and debug options:
-./build_linux.sh -j N # limit to N cores
-./build_linux.sh -1 # single core build
-./build_linux.sh -b # Debug build
-./build_linux.sh -e # RelWithDebInfo build
-./build_linux.sh -c # clean build
-./build_linux.sh -r # skip RAM/disk checks
-./build_linux.sh -l # use Clang instead of GCC
```
### Build test:
@@ -91,6 +46,7 @@ cmake --build build/arm64 --config RelWithDebInfo --target all --
```
+
### Build System
- Uses CMake with minimum version 3.13 (maximum 3.31.x on Windows)
- Primary build directory: `build/`
diff --git a/resources/web/login/orca_login.html b/resources/web/login/orca_login.html
new file mode 100644
index 0000000000..69eec2aedb
--- /dev/null
+++ b/resources/web/login/orca_login.html
@@ -0,0 +1,927 @@
+
+
+
+
+
+ OrcaCloud Login
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Forgot password?
+
+
or continue with
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/scripts/test_moonraker_lane_data.py b/scripts/test_moonraker_lane_data.py
new file mode 100755
index 0000000000..44a0a7e30e
--- /dev/null
+++ b/scripts/test_moonraker_lane_data.py
@@ -0,0 +1,477 @@
+#!/usr/bin/env python3
+"""
+Test script for MoonrakerPrinterAgent filament sync feature.
+Inserts/deletes/modifies random lane data in Moonraker database,
+then reads back and displays with colored output.
+"""
+
+import requests
+import random
+import argparse
+import json
+import time
+import sys
+
+# Configuration
+DEFAULT_HOST = "192.168.88.9"
+DEFAULT_PORT = 7125
+NAMESPACE = "lane_data"
+LANE_KEYS = [f"lane{i}" for i in range(1, 9)] # lane1-lane8
+MATERIALS = ["PLA", "ABS", "PETG", "ASA", "ASA Sparkle", "TPU", ""]
+
+# Material default temperatures (None = use null)
+MATERIAL_TEMPS = {
+ "PLA": {"nozzle": 210, "bed": 60},
+ "ABS": {"nozzle": 240, "bed": 100},
+ "PETG": {"nozzle": 235, "bed": 80},
+ "ASA": {"nozzle": 245, "bed": 105},
+ "ASA Sparkle":{"nozzle": 245, "bed": 105},
+ "TPU": {"nozzle": 220, "bed": 50},
+ "": {"nozzle": None, "bed": None},
+}
+
+def test_connection(host, port, api_key=None, verbose=False):
+ """Test basic connectivity to Moonraker."""
+ url = f"http://{host}:{port}/server/info"
+ headers = {"X-Api-Key": api_key} if api_key else {}
+
+ if verbose:
+ print(f" Testing: GET {url}")
+
+ try:
+ resp = requests.get(url, headers=headers, timeout=10)
+ if verbose:
+ print(f" Response: HTTP {resp.status_code}")
+ if resp.status_code == 200:
+ data = resp.json()
+ if verbose:
+ print(f" Moonraker version: {data.get('result', {}).get('moonraker_version', 'unknown')}")
+ return True
+ else:
+ print(f" Server returned HTTP {resp.status_code}")
+ if verbose:
+ print(f" Response: {resp.text[:500]}")
+ return False
+ except requests.exceptions.ConnectionError as e:
+ print(f" Connection error: {e}")
+ return False
+ except requests.exceptions.Timeout:
+ print(f" Connection timed out")
+ return False
+ except Exception as e:
+ print(f" Error: {type(e).__name__}: {e}")
+ return False
+
+def hex_to_rgb(hex_color):
+ """Convert hex color to RGB tuple."""
+ hex_color = hex_color.lstrip('#')
+ if hex_color.startswith('0x') or hex_color.startswith('0X'):
+ hex_color = hex_color[2:]
+ if len(hex_color) == 6:
+ return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
+ return (128, 128, 128) # Default gray
+
+def color_block(hex_color):
+ """Return ANSI color block for terminal display."""
+ r, g, b = hex_to_rgb(hex_color)
+ return f"\033[48;2;{r};{g};{b}m \033[0m"
+
+def random_color():
+ """Generate random hex color, occasionally returning empty or '#None' like real data."""
+ r = random.random()
+ if r < 0.1:
+ return "" # Empty color (empty lane)
+ if r < 0.15:
+ return "#None" # Observed in real data for unknown colors
+ return "#{:06x}".format(random.randint(0, 0xFFFFFF))
+
+def get_lane_data(host, port, api_key=None):
+ """Fetch all lane data from Moonraker database."""
+ url = f"http://{host}:{port}/server/database/item"
+ params = {"namespace": NAMESPACE}
+ headers = {"X-Api-Key": api_key} if api_key else {}
+
+ try:
+ resp = requests.get(url, params=params, headers=headers, timeout=5)
+ if resp.status_code == 200:
+ data = resp.json()
+ return data.get("result", {}).get("value", {})
+ elif resp.status_code == 404:
+ return {} # Namespace doesn't exist yet
+ else:
+ print(f"Error fetching lane data: HTTP {resp.status_code}")
+ return None
+ except Exception as e:
+ print(f"Error fetching lane data: {e}")
+ return None
+
+def set_lane_data(host, port, lane_key, lane_data, api_key=None):
+ """Set lane data in Moonraker database."""
+ url = f"http://{host}:{port}/server/database/item"
+ headers = {"Content-Type": "application/json"}
+ if api_key:
+ headers["X-Api-Key"] = api_key
+
+ payload = {
+ "namespace": NAMESPACE,
+ "key": lane_key,
+ "value": lane_data
+ }
+
+ try:
+ resp = requests.post(url, json=payload, headers=headers, timeout=5)
+ return resp.status_code == 200
+ except Exception as e:
+ print(f"Error setting lane data: {e}")
+ return False
+
+def delete_lane_data(host, port, lane_key, api_key=None):
+ """Delete lane data from Moonraker database."""
+ url = f"http://{host}:{port}/server/database/item"
+ params = {"namespace": NAMESPACE, "key": lane_key}
+ headers = {"X-Api-Key": api_key} if api_key else {}
+
+ try:
+ resp = requests.delete(url, params=params, headers=headers, timeout=5)
+ return resp.status_code == 200
+ except Exception as e:
+ print(f"Error deleting lane data: {e}")
+ return False
+
+def display_lanes(lanes):
+ """Display lane data with color blocks."""
+ print("\n" + "="*70)
+ print("CURRENT LANE DATA")
+ print("="*70)
+
+ if not lanes:
+ print(" (no lanes configured)")
+ return
+
+ # Sort by lane number
+ sorted_lanes = sorted(lanes.items(),
+ key=lambda x: int(x[1].get("lane", "0")) if x[1].get("lane", "").isdigit() else 0)
+
+ for lane_key, data in sorted_lanes:
+ lane_num = data.get("lane", "?")
+ material = data.get("material", "") or "(empty)"
+ color = data.get("color", "")
+ bed_temp = data.get("bed_temp")
+ nozzle_temp = data.get("nozzle_temp")
+ spool_id = data.get("spool_id")
+
+ # Show color block only for valid hex colors
+ if color and color.startswith("#") and color != "#None" and len(color) == 7:
+ block = color_block(color)
+ else:
+ block = " " # No color block
+
+ bed_str = f"{bed_temp}°C" if bed_temp is not None else "-"
+ noz_str = f"{nozzle_temp}°C" if nozzle_temp is not None else "-"
+ spool_str = f" Spool: {spool_id}" if spool_id is not None else ""
+ color_str = color if color else "(none)"
+
+ print(f" {lane_key} (T{lane_num}): {block} {color_str:10s} {material:12s} "
+ f"Nozzle: {noz_str:6s} Bed: {bed_str:5s}{spool_str}")
+
+ print("="*70 + "\n")
+
+def make_lane_entry(tool_number, material=None):
+ """Generate a lane data entry matching real Moonraker AFC structure."""
+ if material is None:
+ material = random.choice(MATERIALS)
+ temps = MATERIAL_TEMPS[material]
+ color = random_color()
+
+ bed = None
+ nozzle = None
+ if temps["bed"] is not None:
+ bed = temps["bed"] + random.randint(-5, 5)
+ if temps["nozzle"] is not None:
+ nozzle = temps["nozzle"] + random.randint(-10, 10)
+
+ spool_id = random.choice([None, random.randint(1, 50)])
+
+ return {
+ "color": color,
+ "material": material,
+ "bed_temp": bed,
+ "nozzle_temp": nozzle,
+ "scan_time": "",
+ "td": "",
+ "lane": str(tool_number),
+ "spool_id": spool_id,
+ }
+
+def get_used_tool_numbers(host, port, api_key=None, exclude_key=None):
+ """Get set of tool numbers currently in use."""
+ lanes = get_lane_data(host, port, api_key) or {}
+ used = set()
+ for key, data in lanes.items():
+ if key == exclude_key:
+ continue
+ lane_val = data.get("lane", "")
+ if lane_val.isdigit():
+ used.add(int(lane_val))
+ return used
+
+def pick_available_tool_number(used_tool_numbers):
+ """Pick a random tool number (0-7) not already in use. Returns None if all taken."""
+ available = [n for n in range(8) if n not in used_tool_numbers]
+ if not available:
+ return None
+ return random.choice(available)
+
+def fix_duplicate_lanes(host, port, lanes, api_key=None):
+ """Detect and fix duplicate tool numbers in existing lane data.
+
+ Returns the updated lane data after fixes.
+ """
+ if not lanes:
+ return lanes
+
+ # Map tool number -> list of lane keys using it
+ tool_to_keys = {}
+ for key, data in lanes.items():
+ tool = data.get("lane", "")
+ if tool == "":
+ continue
+ tool_to_keys.setdefault(tool, []).append(key)
+
+ # Find duplicates
+ duplicates = {tool: keys for tool, keys in tool_to_keys.items() if len(keys) > 1}
+ if not duplicates:
+ return lanes
+
+ print("DUPLICATE TOOL NUMBERS DETECTED:")
+ for tool, keys in duplicates.items():
+ print(f" Tool T{tool} used by: {', '.join(keys)}")
+
+ # Collect all used tool numbers
+ used = set()
+ for tool, keys in tool_to_keys.items():
+ if tool.isdigit():
+ used.add(int(tool))
+
+ # Fix: keep the first key for each tool, reassign the rest
+ print("\nFixing duplicates...")
+ for tool, keys in duplicates.items():
+ # Keep the first one, reassign the rest
+ for key in keys[1:]:
+ available = [n for n in range(8) if n not in used]
+ if not available:
+ print(f" {key}: cannot fix, no available tool numbers!")
+ continue
+
+ new_tool = available[0]
+ used.add(new_tool)
+
+ lanes[key]["lane"] = str(new_tool)
+ if set_lane_data(host, port, key, lanes[key], api_key):
+ print(f" {key}: T{tool} -> T{new_tool}")
+ else:
+ print(f" {key}: FAILED to update")
+
+ print()
+ return lanes
+
+def perform_random_operations(host, port, api_key=None, num_ops=5):
+ """Perform random insert/modify/delete operations."""
+ operations = ["insert", "modify", "delete"]
+
+ print(f"\nPerforming {num_ops} random operations...")
+ print("-"*50)
+
+ for i in range(num_ops):
+ op = random.choice(operations)
+ lane_key = random.choice(LANE_KEYS)
+
+ if op in ("insert", "modify"):
+ # Get currently used tool numbers, excluding this key (ok to reuse its own)
+ used = get_used_tool_numbers(host, port, api_key, exclude_key=lane_key)
+ tool_num = pick_available_tool_number(used)
+ if tool_num is None:
+ print(f" [{op.upper()}] {lane_key}: SKIPPED (all tool numbers in use)")
+ continue
+
+ lane_data = make_lane_entry(tool_num)
+ action = "INSERT" if op == "insert" else "MODIFY"
+ color = lane_data["color"]
+ material = lane_data["material"] or "(empty)"
+ tool = lane_data["lane"]
+
+ if color and color.startswith("#") and color != "#None" and len(color) == 7:
+ block = color_block(color)
+ else:
+ block = " "
+
+ if set_lane_data(host, port, lane_key, lane_data, api_key):
+ print(f" [{action}] {lane_key} (T{tool}): {block} {color or '(none)'} "
+ f"{material} spool={lane_data['spool_id']}")
+ else:
+ print(f" [{action}] {lane_key}: FAILED")
+
+ elif op == "delete":
+ if delete_lane_data(host, port, lane_key, api_key):
+ print(f" [DELETE] {lane_key}")
+ else:
+ print(f" [DELETE] {lane_key}: FAILED (may not exist)")
+
+ time.sleep(0.1) # Small delay between operations
+
+ print("-"*50)
+
+def load_lanes_from_file(filepath, host, port, api_key=None):
+ """Load lane data from a JSON file and overwrite all lanes on the printer.
+
+ Accepts either the raw Moonraker response format:
+ {"result": {"namespace": "lane_data", "value": {"lane1": {...}, ...}}}
+ or the plain value object:
+ {"lane1": {...}, "lane2": {...}, ...}
+ """
+ try:
+ with open(filepath, "r") as f:
+ data = json.load(f)
+ except FileNotFoundError:
+ print(f"Error: file not found: {filepath}")
+ return False
+ except json.JSONDecodeError as e:
+ print(f"Error: invalid JSON in {filepath}: {e}")
+ return False
+
+ # Accept both wrapped and unwrapped formats
+ if "result" in data and "value" in data.get("result", {}):
+ lanes = data["result"]["value"]
+ else:
+ lanes = data
+
+ if not isinstance(lanes, dict):
+ print(f"Error: expected object with lane keys, got {type(lanes).__name__}")
+ return False
+
+ # Validate no duplicate tool numbers
+ tool_to_keys = {}
+ for key, entry in lanes.items():
+ tool = entry.get("lane", "")
+ if tool:
+ tool_to_keys.setdefault(tool, []).append(key)
+ dupes = {t: keys for t, keys in tool_to_keys.items() if len(keys) > 1}
+ if dupes:
+ print("Error: input JSON has duplicate tool numbers:")
+ for tool, keys in dupes.items():
+ print(f" Tool T{tool} used by: {', '.join(keys)}")
+ return False
+
+ print(f"Loading {len(lanes)} lane(s) from {filepath}...")
+
+ # Clear all existing lanes first
+ print(" Clearing existing lanes...")
+ for lane_key in LANE_KEYS:
+ delete_lane_data(host, port, lane_key, api_key)
+
+ # Write each lane from the file
+ ok = True
+ for lane_key, lane_data in lanes.items():
+ if set_lane_data(host, port, lane_key, lane_data, api_key):
+ tool = lane_data.get("lane", "?")
+ material = lane_data.get("material", "") or "(empty)"
+ color = lane_data.get("color", "")
+ if color and color.startswith("#") and color != "#None" and len(color) == 7:
+ block = color_block(color)
+ else:
+ block = " "
+ print(f" [LOAD] {lane_key} (T{tool}): {block} {color or '(none)'} {material}")
+ else:
+ print(f" [LOAD] {lane_key}: FAILED")
+ ok = False
+
+ return ok
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="Test Moonraker lane data for MoonrakerPrinterAgent filament sync"
+ )
+ parser.add_argument("--host", default=DEFAULT_HOST,
+ help=f"Moonraker host (default: {DEFAULT_HOST})")
+ parser.add_argument("--port", type=int, default=DEFAULT_PORT,
+ help=f"Moonraker port (default: {DEFAULT_PORT})")
+ parser.add_argument("--api-key", help="Moonraker API key (if required)")
+ parser.add_argument("--ops", type=int, default=5,
+ help="Number of random operations (default: 5)")
+ parser.add_argument("--clear", action="store_true",
+ help="Clear all lane data before starting")
+ parser.add_argument("--read-only", action="store_true",
+ help="Only read and display current lane data")
+ parser.add_argument("--load", metavar="FILE",
+ help="Load lane data from JSON file and overwrite printer lanes")
+ parser.add_argument("--verbose", "-v", action="store_true",
+ help="Verbose output for debugging")
+
+ args = parser.parse_args()
+
+ print(f"\nConnecting to Moonraker at {args.host}:{args.port}...")
+
+ # First test basic connectivity
+ if not test_connection(args.host, args.port, args.api_key, args.verbose):
+ print("\nFailed to connect to Moonraker!")
+ print("\nTroubleshooting:")
+ print(f" 1. Check if Moonraker is running on {args.host}")
+ print(f" 2. Verify port {args.port} is correct (default Moonraker port is 7125)")
+ print(f" 3. Try: curl http://{args.host}:{args.port}/server/info")
+ print(f" 4. Check if API key is required (--api-key)")
+ return 1
+
+ print("Connected!")
+
+ # Now fetch lane data
+ current = get_lane_data(args.host, args.port, args.api_key)
+ if current is None:
+ print("Connected to Moonraker but failed to fetch lane data!")
+ return 1
+
+ # Check for and fix duplicate tool numbers
+ current = fix_duplicate_lanes(args.host, args.port, current, args.api_key)
+
+ # Show current state
+ display_lanes(current)
+
+ if args.read_only:
+ return 0
+
+ # Load from JSON file if requested
+ if args.load:
+ if not load_lanes_from_file(args.load, args.host, args.port, args.api_key):
+ return 1
+ final = get_lane_data(args.host, args.port, args.api_key)
+ display_lanes(final)
+ if final is not None:
+ print("RAW JSON:")
+ print(json.dumps({"result": {"namespace": NAMESPACE, "key": None, "value": final}}, indent=2))
+ print()
+ return 0
+
+ # Clear if requested
+ if args.clear:
+ print("Clearing all lane data...")
+ for lane_key in LANE_KEYS:
+ delete_lane_data(args.host, args.port, lane_key, args.api_key)
+ print("Cleared!")
+ display_lanes({})
+
+ # Perform random operations
+ perform_random_operations(args.host, args.port, args.api_key, args.ops)
+
+ # Read back and display final state
+ final = get_lane_data(args.host, args.port, args.api_key)
+ display_lanes(final)
+
+ # Print raw JSON
+ if final is not None:
+ print("RAW JSON:")
+ print(json.dumps({"result": {"namespace": NAMESPACE, "key": None, "value": final}}, indent=2))
+ print()
+
+ return 0
+
+if __name__ == "__main__":
+ exit(main())
diff --git a/src/libslic3r/AppConfig.cpp b/src/libslic3r/AppConfig.cpp
index c9ce68e3e0..e963948d98 100644
--- a/src/libslic3r/AppConfig.cpp
+++ b/src/libslic3r/AppConfig.cpp
@@ -300,6 +300,13 @@ void AppConfig::set_defaults()
if (get("allow_abnormal_storage").empty()) {
set_bool("allow_abnormal_storage", false);
}
+#ifdef __linux__
+ if (get(SETTING_USE_ENCRYPTED_TOKEN_FILE).empty())
+ set_bool(SETTING_USE_ENCRYPTED_TOKEN_FILE, true);
+#else
+ if (get(SETTING_USE_ENCRYPTED_TOKEN_FILE).empty())
+ set_bool(SETTING_USE_ENCRYPTED_TOKEN_FILE, false);
+#endif
if(get("check_stable_update_only").empty()) {
set_bool("check_stable_update_only", false);
diff --git a/src/libslic3r/AppConfig.hpp b/src/libslic3r/AppConfig.hpp
index b521410ce9..e501860092 100644
--- a/src/libslic3r/AppConfig.hpp
+++ b/src/libslic3r/AppConfig.hpp
@@ -28,6 +28,7 @@ using namespace nlohmann;
#define SETTING_NETWORK_PLUGIN_SKIPPED_VERSIONS "network_plugin_skipped_versions"
#define SETTING_NETWORK_PLUGIN_UPDATE_DISABLED "network_plugin_update_prompts_disabled"
#define SETTING_NETWORK_PLUGIN_REMIND_LATER "network_plugin_remind_later"
+#define SETTING_USE_ENCRYPTED_TOKEN_FILE "use_encrypted_token_file"
#define BAMBU_NETWORK_AGENT_VERSION_LEGACY "01.10.01.01"
#define SUPPORT_DARK_MODE
diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp
index f230a11087..d0353e2ed1 100644
--- a/src/libslic3r/Preset.cpp
+++ b/src/libslic3r/Preset.cpp
@@ -1017,7 +1017,7 @@ static std::vector s_Preset_printer_options {
"scan_first_layer", "enable_power_loss_recovery", "wrapping_detection_layers", "wrapping_exclude_area", "machine_load_filament_time", "machine_unload_filament_time", "machine_tool_change_time", "time_cost", "machine_pause_gcode", "template_custom_gcode",
"nozzle_type", "nozzle_hrc","auxiliary_fan", "nozzle_volume","upward_compatible_machine", "z_hop_types", "travel_slope", "retract_lift_enforce","support_chamber_temp_control","support_air_filtration","printer_structure",
"best_object_pos", "head_wrap_detect_zone",
- "host_type", "print_host", "printhost_apikey", "bbl_use_printhost",
+ "host_type", "print_host", "printhost_apikey", "bbl_use_printhost", "printer_agent",
"print_host_webui",
"printhost_cafile","printhost_port","printhost_authorization_type",
"printhost_user", "printhost_password", "printhost_ssl_ignore_revoke", "thumbnails", "thumbnails_format",
@@ -1502,7 +1502,7 @@ int PresetCollection::get_differed_values_to_update(Preset& preset, std::map size_t {
+ for (size_t i = start; i < m_presets.size(); ++i) {
+ const auto& p = m_presets[i];
+ if (p.is_visible && p.is_compatible && p.is_system
+ && get_preset_base(p) == &p
+ && p.config.opt_string("filament_type", 0u) == target)
+ return i;
+ }
+ return size_t(-1);
+ };
+
+ // 1. Exact filament_type match
+ size_t idx = find_by_type(filament_type);
+ if (idx != size_t(-1))
+ return idx;
+
+ // 2. Base type fallback: strip modifier after first space
+ // e.g. "PLA High Speed" -> "PLA"
+ // Dash-separated types like "PA-CF", "PET-CF" are distinct materials, not modifiers.
+ auto sep = filament_type.find(' ');
+ if (sep != std::string::npos) {
+ idx = find_by_type(filament_type.substr(0, sep));
+ if (idx != size_t(-1))
+ return idx;
+ }
+
+ // 3. Any visible preset
+ return first_visible_idx();
+}
+
+std::string PresetCollection::filament_id_by_type(const std::string& filament_type) const
+{
+ return preset(first_visible_idx_by_type(filament_type)).filament_id;
+}
+
std::vector PresetCollection::diameters_of_selected_printer()
{
std::set diameters;
@@ -3397,6 +3437,7 @@ static std::vector s_PhysicalPrinter_opts {
"printer_technology",
"bbl_use_printhost",
"host_type",
+ "printer_agent",
"print_host",
"print_host_webui",
"printhost_apikey",
diff --git a/src/libslic3r/Preset.hpp b/src/libslic3r/Preset.hpp
index e1c3f57f33..d76f2b93c2 100644
--- a/src/libslic3r/Preset.hpp
+++ b/src/libslic3r/Preset.hpp
@@ -53,7 +53,9 @@
#define BBL_JSON_KEY_BASE_ID "base_id"
#define BBL_JSON_KEY_USER_ID "user_id"
#define BBL_JSON_KEY_FILAMENT_ID "filament_id"
-#define BBL_JSON_KEY_UPDATE_TIME "updated_time"
+#define UNKNOWN_FILAMENT_ID "__unknown__"
+#define ORCA_JSON_KEY_UPDATE_TIME "updated_time"
+#define ORCA_JSON_KEY_CREATED_TIME "created_time"
#define BBL_JSON_KEY_INHERITS "inherits"
#define BBL_JSON_KEY_INSTANTIATION "instantiation"
#define BBL_JSON_KEY_NOZZLE_DIAMETER "nozzle_diameter"
@@ -636,6 +638,11 @@ public:
return const_cast(this)->find_preset2(name, auto_match);
}
size_t first_visible_idx() const;
+ // Return the index of the first visible, compatible, system base preset
+ // matching the given filament_type. Falls back to base type, then any visible.
+ size_t first_visible_idx_by_type(const std::string& filament_type) const;
+ // Return the filament_id of the best-matching visible preset for the given filament type.
+ std::string filament_id_by_type(const std::string& filament_type) const;
// Return index of the first compatible preset. Certainly at least the '- default -' preset shall be compatible.
// If one of the prefered_alternates is compatible, select it.
template size_t first_compatible_idx(PreferedCondition prefered_condition) const
diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp
index 0ee55726d7..576080f51e 100644
--- a/src/libslic3r/PresetBundle.cpp
+++ b/src/libslic3r/PresetBundle.cpp
@@ -2208,8 +2208,14 @@ void PresetBundle::set_num_filaments(unsigned int n, std::string new_color)
ConfigOptionStrings *filament_multi_color = project_config.option("filament_multi_colour");
ConfigOptionStrings* filament_color_type = project_config.option("filament_colour_type");
ConfigOptionInts* filament_map = project_config.option("filament_map");
+
+
filament_color->resize(n);
- filament_multi_color->resize(n);
+ // Sync filament multi colour
+ filament_multi_color->values.resize(n);
+ for (size_t i = 0; i < n; i++) {
+ filament_multi_color->values[i] = filament_color->values[i];
+ }
filament_color_type->resize(n);
filament_map->values.resize(n, 1);
ams_multi_color_filment.resize(n);
@@ -2345,6 +2351,7 @@ unsigned int PresetBundle::sync_ams_list(std::vector("filament_multi_colour")->values;
auto ams_id = ams.opt_string("ams_id", 0u);
auto slot_id = ams.opt_string("slot_id", 0u);
- ams_infos.push_back({filament_id.empty() ? false : true,false, filament_color});
+ auto is_placeholder = ams.has("filament_slot_placeholder") && ams.opt_bool("filament_slot_placeholder", 0u);
+ ams_infos.push_back({filament_id.empty() ? false : true, false, is_placeholder, filament_color});
AMSMapInfo temp = {ams_id, slot_id};
ams_array_maps.push_back(temp);
index++;
@@ -2381,6 +2389,12 @@ unsigned int PresetBundle::sync_ams_list(std::vector(haystack[pos - 1])));
+ bool end_ok = (pos + needle.size() >= haystack.size() ||
+ !std::isalnum(static_cast(haystack[pos + needle.size()])));
+ if (start_ok && end_ok)
+ return true;
+ pos = haystack.find(needle, pos + 1);
+ }
+ return false;
+ };
+ // Find the longest-matching preset type to prefer e.g. "PA-CF" over "PA".
+ size_t best_len = 0;
+ for (auto it = filaments.begin(); it != filaments.end(); ++it) {
+ if (!it->is_compatible || !it->is_system || !boost::algorithm::starts_with(it->name, "Generic "))
+ continue;
+ auto preset_type = boost::to_upper_copy(it->config.opt_string("filament_type", 0u));
+ if (preset_type.size() > best_len && contains_word(upper_type, preset_type)) {
+ iter = it;
+ best_len = preset_type.size();
+ filament_type = "Generic " + it->config.opt_string("filament_type", 0u);
+ }
+ }
+ }
}
if (iter == filaments.end()) {
// Prefer old selection
@@ -2417,8 +2461,13 @@ unsigned int PresetBundle::sync_ams_list(std::vectorvalues.resize(exist_filament_presets.size(), 1);
}
else {//overwrite;
- filament_color->values = ams_filament_colors;
- filament_color_type->values = ams_filament_color_types;
- this->filament_presets = ams_filament_presets;
- filament_map->values.resize(ams_filament_colors.size(), 1);
+ bool has_placeholders = std::any_of(ams_infos.begin(), ams_infos.end(),
+ [](const AmsInfo& a) { return a.is_placeholder; });
+ if (has_placeholders) {
+ // Orca: merge — keep existing filaments for empty slots
+ auto exist_colors = filament_color->values;
+ auto exist_color_types = filament_color_type->values;
+ auto exist_presets = this->filament_presets;
+
+ size_t tray_count = ams_filament_presets.size();
+ size_t total = std::max(tray_count, exist_presets.size());
+
+ std::vector result_colors;
+ std::vector result_color_types;
+ std::vector result_presets;
+ std::vector> result_multi_colors;
+
+ for (size_t i = 0; i < total; i++) {
+ bool is_loaded = (i < ams_infos.size() && ams_infos[i].valid);
+
+ if (is_loaded) {
+ // Loaded tray: use tray's filament data
+ result_colors.push_back(ams_filament_colors[i]);
+ result_color_types.push_back(ams_filament_color_types[i]);
+ result_presets.push_back(ams_filament_presets[i]);
+ result_multi_colors.push_back(
+ i < ams_multi_color_filment.size() ? ams_multi_color_filment[i]
+ : std::vector{ams_filament_colors[i]});
+ } else if (i < exist_presets.size()) {
+ // Empty tray or beyond tray count: keep existing filament
+ result_colors.push_back(exist_colors[i]);
+ result_color_types.push_back(exist_color_types[i]);
+ result_presets.push_back(exist_presets[i]);
+ result_multi_colors.push_back({exist_colors[i]});
+ } else {
+ // New slot beyond existing count: prefer a generic filament preset
+ auto it = std::find_if(filaments.begin(), filaments.end(), [](const Preset &f) {
+ return f.is_compatible && f.is_system
+ && boost::algorithm::starts_with(f.name, "Generic ");
+ });
+ std::string fallback_name = (it != filaments.end()) ? it->name : filaments.first_visible().name;
+ result_colors.push_back("#CECECE");
+ result_color_types.push_back("1");
+ result_presets.push_back(fallback_name);
+ result_multi_colors.push_back({"#CECECE"});
+ }
+ }
+
+ filament_color->values = result_colors;
+ filament_color_type->values = result_color_types;
+ this->filament_presets = result_presets;
+ ams_multi_color_filment = result_multi_colors;
+ filament_map->values.resize(total, 1);
+ } else {
+ // BBL: existing wholesale replace
+ filament_color->values = ams_filament_colors;
+ filament_color_type->values = ams_filament_color_types;
+ this->filament_presets = ams_filament_presets;
+ filament_map->values.resize(ams_filament_colors.size(), 1);
+ }
auto& print_config = this->prints.get_edited_preset().config;
auto support_filament_opt = print_config.option("support_filament");
auto support_interface_filament_opt = print_config.option("support_interface_filament");
- if (support_filament_opt->value > ams_filament_color_types.size())
+ if (support_filament_opt->value > filament_color_type->values.size())
support_filament_opt->value = 0;
- if (support_interface_filament_opt->value > ams_filament_color_types.size())
+ if (support_interface_filament_opt->value > filament_color_type->values.size())
support_interface_filament_opt->value = 0;
}
// Update ams_multi_color_filment
diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp
index 1d3c2cb963..b264814de5 100644
--- a/src/libslic3r/PrintConfig.cpp
+++ b/src/libslic3r/PrintConfig.cpp
@@ -740,6 +740,13 @@ void PrintConfigDef::init_common_params()
def->cli = ConfigOptionDef::nocli;
def->set_default_value(new ConfigOptionBool(false));
+ def = this->add("printer_agent", coString);
+ def->label = L("Printer Agent");
+ def->tooltip = L("Select the network agent implementation for printer communication.");
+ def->mode = comAdvanced;
+ def->cli = ConfigOptionDef::nocli;
+ def->set_default_value(new ConfigOptionString(""));
+
def = this->add("print_host", coString);
def->label = L("Hostname, IP or URL");
def->tooltip = L("Orca Slicer can upload G-code files to a printer host. This field should contain "
diff --git a/src/libslic3r/Time.cpp b/src/libslic3r/Time.cpp
index 8faa14ade3..81b9af49e2 100644
--- a/src/libslic3r/Time.cpp
+++ b/src/libslic3r/Time.cpp
@@ -6,6 +6,8 @@
#include
#include
#include
+#include
+#include
#ifdef _MSC_VER
#include