diff --git a/CHANGELOG_v0.8.md b/CHANGELOG_v0.8.md new file mode 100644 index 0000000000..9bd4aa5734 --- /dev/null +++ b/CHANGELOG_v0.8.md @@ -0,0 +1,61 @@ +## v0.8 Pre-release + +EXPERIMENTAL BUILD - LIMITED TESTING +Based on Snapmaker Orca v2.2.4 + +v0.8 full spectrum pre-release focuses on filament mixer color accuracy and Linux AppImage packaging. + +This has had limited testing on the Snapmaker U1 via the community. + +### What's New in v0.8 + +#### Filament Mixer Color Blending (v0.8) +- Replaced the legacy RYB blend path with FilamentMixer. +- Updated mixed-filament preview/display color computation to use FilamentMixer interpolation. +- Improved mixed-filament color consistency for generated combinations. +- Retained legacy RYB conversion helpers in code as reference. + +#### Linux AppImage Packaging (new in v0.8) +- Added Linux release artifact: `Snapmaker_Orca_Linux_V2.2.4.AppImage`. +- AppImage is now the primary Linux release artifact for end users. +- Current Linux build target is `x86_64`. +- Current runtime baseline is glibc `2.38+`. +- `Snapmaker_Orca.tar` remains available as an advanced/manual fallback. + +### Installation + +#### Windows +1. Download `Snapmaker_Orca.zip`. +2. Extract to a folder. +3. Run the executable. + +#### macOS +1. Download the macOS build (`arm64` for Apple Silicon or `x86_64` for Intel). +2. If the release asset is a `.zip`, unzip it first. +3. Open the `.dmg`. +4. Drag `Snapmaker_Orca.app` into `Applications`. +5. Launch the app from `Applications`. + +#### Linux (AppImage) +1. Download `Snapmaker_Orca_Linux_V2.2.4.AppImage`. +2. Run `chmod +x Snapmaker_Orca_Linux_V2.2.4.AppImage`. +3. Run `./Snapmaker_Orca_Linux_V2.2.4.AppImage`. + +### Warning +- Use at your own risk. +- May produce incorrect G-code in edge cases. +- Mixed-filament behavior is still experimental in some scenarios. +- This release has had limited real-printer validation. + +### Features Not Yet Fully Tested +1. Linux AppImage behavior across all desktop environments/distributions. +2. Mixed-filament visual color matching across different filament brands/materials. + +### Known Issues +- Older Linux distributions may fail to run this AppImage due to glibc mismatch. +- Some systems may require `libfuse2` for AppImage execution. +- On-screen color blend preview may not exactly match physical print results. + +### Credits +- FilamentMixer color blending integration is powered by the FilamentMixer library by [justinh-rahb](https://github.com/justinh-rahb). +- Library repository: [https://github.com/justinh-rahb/filament-mixer](https://github.com/justinh-rahb/filament-mixer). diff --git a/README.md b/README.md index 4cdceae070..664952877c 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,14 @@ --- +## ☕ Support Development + +If you find this fun or interesting! + +Buy Me A Coffee + +--- + ## ⚠️ **IMPORTANT DISCLAIMER** ⚠️ **This fork is currently in active development and has NOT been tested on actual hardware! ** diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 4b62879613..63c7d8d24f 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -5175,8 +5175,12 @@ LayerResult GCode::process_layer(const Print& print, m_config.apply(instance_to_print.print_object.config(), true); m_layer = layer_to_print.layer(); m_object_layer_over_raft = object_layer_over_raft; - if (m_config.reduce_crossing_wall) - m_avoid_crossing_perimeters.init_layer(*m_layer); + const bool saved_reduce_crossing_wall = m_config.reduce_crossing_wall.value; + // Local-Z phase-b emits many short micro-passes. Avoid-crossing + // travel planning is expensive and fragile on these fragments, so + // keep travel simple here. + m_config.reduce_crossing_wall.value = false; + m_avoid_crossing_perimeters.disable_once(); const Point& offset = instance_to_print.print_object.instances()[instance_to_print.instance_id].shift; std::pair this_object_copy(&instance_to_print.print_object, offset); @@ -5189,6 +5193,7 @@ LayerResult GCode::process_layer(const Print& print, gcode += this->extrude_perimeters(print, island.by_region, first_layer, false); gcode += this->extrude_perimeters(print, island.by_region, first_layer, true); } + m_config.reduce_crossing_wall.value = saved_reduce_crossing_wall; } } m_nominal_z = saved_nominal_z; diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 136ebf91cf..1991196a0d 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -1015,6 +1015,21 @@ static bool fit_pass_heights_to_interval(std::vector &passes, double bas return std::all_of(passes.begin(), passes.end(), within); } +static bool sanitize_local_z_pass_heights(std::vector &passes, double base_height, double lower_bound, double upper_bound) +{ + if (passes.empty() || base_height <= EPSILON) + return false; + + const double lo = std::max(0.01, lower_bound); + const double hi = std::max(lo, upper_bound); + for (double &h : passes) { + if (!std::isfinite(h)) + h = lo; + h = std::clamp(h, lo, hi); + } + return fit_pass_heights_to_interval(passes, base_height, lo, hi); +} + static std::vector build_uniform_local_z_pass_heights(double base_height, double lo, double hi) { std::vector out; @@ -1079,7 +1094,6 @@ static std::vector build_local_z_alternating_pass_heights(double base_he const double cycle_h = std::max(EPSILON, gradient_h_a + gradient_h_b); const double ratio_a = std::clamp(gradient_h_a / cycle_h, 0.0, 1.0); - const double ratio_b = 1.0 - ratio_a; size_t min_passes = size_t(std::max(2.0, std::ceil((base_height - EPSILON) / hi))); if ((min_passes % 2) != 0) @@ -1091,11 +1105,44 @@ static std::vector build_local_z_alternating_pass_heights(double base_he if (max_passes < 2 || min_passes > max_passes) return build_uniform_local_z_pass_heights(base_height, lo, hi); + const double target_step = 0.5 * (lo + hi); + size_t target_passes = + size_t(std::max(2.0, std::llround(base_height / std::max(target_step, EPSILON)))); + if ((target_passes % 2) != 0) { + const size_t round_up = (target_passes < max_passes) ? (target_passes + 1) : max_passes; + const size_t round_down = (target_passes > min_passes) ? (target_passes - 1) : min_passes; + if (round_up > max_passes) + target_passes = round_down; + else if (round_down < min_passes) + target_passes = round_up; + else { + const size_t up_dist = round_up - target_passes; + const size_t down_dist = target_passes - round_down; + target_passes = (up_dist <= down_dist) ? round_up : round_down; + } + } + target_passes = std::clamp(target_passes, min_passes, max_passes); + + bool has_best = false; + std::vector best_passes; + double best_ratio_error = 0.0; + size_t best_pass_distance = 0; + double best_max_height = 0.0; + size_t best_pass_count = 0; + for (size_t pass_count = min_passes; pass_count <= max_passes; pass_count += 2) { const size_t pair_count = pass_count / 2; - const double pair_h = base_height / double(pair_count); - const double h_a = pair_h * ratio_a; - const double h_b = pair_h * ratio_b; + if (pair_count == 0) + continue; + const double pair_h = base_height / double(pair_count); + + const double h_a_min = std::max(lo, pair_h - hi); + const double h_a_max = std::min(hi, pair_h - lo); + if (h_a_min > h_a_max + EPSILON) + continue; + + const double h_a = std::clamp(pair_h * ratio_a, h_a_min, h_a_max); + const double h_b = pair_h - h_a; std::vector out; out.reserve(pass_count); @@ -1103,13 +1150,68 @@ static std::vector build_local_z_alternating_pass_heights(double base_he out.emplace_back(h_a); out.emplace_back(h_b); } - if (fit_pass_heights_to_interval(out, base_height, lo, hi)) - return out; + if (!fit_pass_heights_to_interval(out, base_height, lo, hi)) + continue; + + const double ratio_actual = (h_a + h_b > EPSILON) ? (h_a / (h_a + h_b)) : 0.5; + const double ratio_error = std::abs(ratio_actual - ratio_a); + const size_t pass_distance = + (pass_count > target_passes) ? (pass_count - target_passes) : (target_passes - pass_count); + const double max_height = std::max(h_a, h_b); + + const bool better_ratio = !has_best || (ratio_error + 1e-6 < best_ratio_error); + const bool similar_ratio = has_best && std::abs(ratio_error - best_ratio_error) <= 1e-6; + const bool better_distance = similar_ratio && (pass_distance < best_pass_distance); + const bool similar_distance = similar_ratio && (pass_distance == best_pass_distance); + const bool better_max_height = similar_distance && (max_height + 1e-6 < best_max_height); + const bool similar_max_height = similar_distance && std::abs(max_height - best_max_height) <= 1e-6; + const bool better_pass_count = similar_max_height && (pass_count > best_pass_count); + + if (better_ratio || better_distance || better_max_height || better_pass_count) { + has_best = true; + best_passes = std::move(out); + best_ratio_error = ratio_error; + best_pass_distance = pass_distance; + best_max_height = max_height; + best_pass_count = pass_count; + } } + if (has_best) + return best_passes; return build_uniform_local_z_pass_heights(base_height, lo, hi); } +static std::vector build_local_z_shared_pass_heights(double base_height, double lower_bound, double upper_bound) +{ + if (base_height <= EPSILON) + return {}; + + const double lo = std::max(0.01, lower_bound); + const double hi = std::max(lo, upper_bound); + if (base_height < 2.0 * lo - EPSILON) + return { base_height }; + + // In shared (dense multi-zone) mode keep a single pair of pass planes for + // the whole nominal layer, anchored to the configured lower / upper bounds. + double h_small = lo; + double h_large = base_height - h_small; + if (h_large > hi + EPSILON) { + h_large = hi; + h_small = base_height - h_large; + } + if (h_small < lo - EPSILON || h_small > hi + EPSILON || + h_large < lo - EPSILON || h_large > hi + EPSILON) + return build_uniform_local_z_pass_heights(base_height, lo, hi); + + std::vector out { h_small, h_large }; + if (!fit_pass_heights_to_interval(out, base_height, lo, hi)) + return build_uniform_local_z_pass_heights(base_height, lo, hi); + if (out.size() == 2 && out[0] > out[1]) + std::swap(out[0], out[1]); + return out; +} + static std::vector build_local_z_pass_heights(double base_height, double lower_bound, double upper_bound, @@ -1360,10 +1462,11 @@ static std::vector pointillism_sequence_for_row(const MixedFilamen static bool local_z_eligible_mixed_row(const MixedFilament &mf) { - // Local-Z flow-height modulation is intended for custom gradient rows. - // Keep auto-generated premix rows and manual pattern rows on nominal layers. + // Local-Z flow-height modulation should apply to all mixed rows that are + // resolved as A/B blends on model surfaces, not only custom rows. + // Exclude explicit manual patterns and same-layer pointillism rows, which + // have their own distribution semantics. return mf.enabled && - mf.custom && mf.manual_pattern.empty() && mf.distribution_mode != int(MixedFilament::SameLayerPointillisme); } @@ -1831,7 +1934,6 @@ static void build_local_z_plan(PrintObject &print_object, const std::vector row_next_component_a(mixed_rows.size(), uint8_t(1)); - std::vector row_phase_initialized(mixed_rows.size(), uint8_t(0)); - - int cadence_index = 0; + size_t shared_multi_row_fallback_intervals = 0; + constexpr size_t LOCAL_Z_MAX_ISOLATED_ACTIVE_ROWS = 2; + constexpr size_t LOCAL_Z_MAX_ISOLATED_MASK_COMPONENTS = 24; + constexpr size_t LOCAL_Z_MAX_ISOLATED_MASK_VERTICES = 1200; + constexpr bool LOCAL_Z_SHARED_FALLBACK_ENABLED = false; + // Keep local-Z cadence isolated per mixed row so independent painted zones + // do not influence each other when resolving fallback cadence. + std::vector row_cadence_index(mixed_rows.size(), 0); + // Reset row cadence at the start of each disjoint painted zone. + std::vector row_active_prev_layer(mixed_rows.size(), uint8_t(0)); for (size_t layer_id = 0; layer_id < print_object.layer_count(); ++layer_id) { throw_on_cancel(); @@ -1912,6 +2018,7 @@ static void build_local_z_plan(PrintObject &print_object, const std::vector row_active_this_layer(mixed_rows.size(), uint8_t(0)); size_t dominant_mixed_idx = size_t(-1); double dominant_mixed_area = -1.0; double dominant_gradient_h_a = 0.0; @@ -1933,6 +2040,7 @@ static void build_local_z_plan(PrintObject &print_object, const std::vector row_state_masks(mixed_rows.size()); + std::vector row_state_ids(mixed_rows.size(), 0); + for (size_t channel_idx = 0; channel_idx < segmentation[layer_id].size(); ++channel_idx) { + const ExPolygons &state_masks = segmentation[layer_id][channel_idx]; + if (state_masks.empty()) + continue; + const unsigned int state_id = unsigned(channel_idx + 1); + if (!mixed_mgr.is_mixed(state_id, num_physical)) + continue; + const int mixed_idx = mixed_mgr.mixed_index_from_filament_id(state_id, num_physical); + if (mixed_idx < 0 || size_t(mixed_idx) >= mixed_rows.size()) + continue; + const size_t row_idx = size_t(mixed_idx); + if (row_active_this_layer[row_idx] == 0 || !local_z_eligible_mixed_row(mixed_rows[row_idx])) + continue; + row_state_ids[row_idx] = state_id; + append(row_state_masks[row_idx], state_masks); + } + for (ExPolygons &state_masks : row_state_masks) + if (state_masks.size() > 1) + state_masks = union_ex(state_masks); + size_t active_row_mask_components = 0; + size_t active_row_mask_vertices = 0; + for (size_t row_idx = 0; row_idx < row_state_masks.size(); ++row_idx) + if (row_active_this_layer[row_idx] != 0) { + active_row_mask_components += row_state_masks[row_idx].size(); + for (const ExPolygon &expoly : row_state_masks[row_idx]) { + active_row_mask_vertices += expoly.contour.points.size(); + for (const Polygon &hole : expoly.holes) + active_row_mask_vertices += hole.points.size(); + } + } + + std::vector> isolated_row_pass_heights(mixed_rows.size()); + bool isolated_multi_row_mode = false; + const bool shared_multi_row_fallback = + LOCAL_Z_SHARED_FALLBACK_ENABLED && + interval.has_mixed_paint && + preferred_a <= EPSILON && + preferred_b <= EPSILON && + active_mixed_rows > 1 && + (active_mixed_rows > LOCAL_Z_MAX_ISOLATED_ACTIVE_ROWS || + active_row_mask_components > LOCAL_Z_MAX_ISOLATED_MASK_COMPONENTS || + active_row_mask_vertices > LOCAL_Z_MAX_ISOLATED_MASK_VERTICES); + if (shared_multi_row_fallback) + ++shared_multi_row_fallback_intervals; + if (interval.has_mixed_paint && + preferred_a <= EPSILON && + preferred_b <= EPSILON && + !shared_multi_row_fallback && + active_mixed_rows > 1) { + size_t isolated_rows_with_split = 0; + for (size_t row_idx = 0; row_idx < row_active_this_layer.size(); ++row_idx) { + if (row_active_this_layer[row_idx] == 0) + continue; + + double row_h_a = 0.0; + double row_h_b = 0.0; + compute_local_z_gradient_component_heights(mixed_rows[row_idx].mix_b_percent, mixed_lower, mixed_upper, row_h_a, row_h_b); + std::vector row_passes = build_local_z_alternating_pass_heights(interval.base_height, + mixed_lower, + mixed_upper, + row_h_a, + row_h_b); + if (row_passes.empty()) + row_passes.emplace_back(interval.base_height); + if (!sanitize_local_z_pass_heights(row_passes, interval.base_height, mixed_lower, mixed_upper)) + row_passes = build_uniform_local_z_pass_heights(interval.base_height, mixed_lower, mixed_upper); + if (row_passes.size() > 1) + ++isolated_rows_with_split; + isolated_row_pass_heights[row_idx] = std::move(row_passes); + } + if (isolated_rows_with_split > 0) { + isolated_multi_row_mode = true; + ++alternating_height_intervals; + } + } + std::vector pass_heights; - if (interval.has_mixed_paint) { + if (interval.has_mixed_paint && !isolated_multi_row_mode) { // Local-Z mode should emit an A/B/A/B pattern for mixed regions and // derive relative heights from mixed-filament gradient bounds. if (preferred_a <= EPSILON && preferred_b <= EPSILON) { - if (dominant_gradient_valid) { + if (shared_multi_row_fallback) { + pass_heights = build_local_z_shared_pass_heights(interval.base_height, mixed_lower, mixed_upper); + if (pass_heights.size() > 1) + ++alternating_height_intervals; + } else if (dominant_gradient_valid) { pass_heights = build_local_z_alternating_pass_heights(interval.base_height, mixed_lower, mixed_upper, dominant_gradient_h_a, dominant_gradient_h_b); if (pass_heights.size() > 1) ++alternating_height_intervals; } else { - pass_heights = build_local_z_pass_heights(interval.base_height, mixed_lower, mixed_upper, preferred_a, preferred_b); + pass_heights = build_uniform_local_z_pass_heights(interval.base_height, mixed_lower, mixed_upper); } } else { pass_heights = build_local_z_pass_heights(interval.base_height, mixed_lower, mixed_upper, preferred_a, preferred_b); @@ -1983,6 +2179,11 @@ static void build_local_z_plan(PrintObject &print_object, const std::vector 1; + size_t pass_count_for_log = pass_heights.size(); + double pass_min_height_for_log = pass_heights.empty() ? 0.0 : *std::min_element(pass_heights.begin(), pass_heights.end()); + double pass_max_height_for_log = pass_heights.empty() ? 0.0 : *std::max_element(pass_heights.begin(), pass_heights.end()); + + const bool split_interval = interval.has_mixed_paint && (isolated_multi_row_mode || pass_heights.size() > 1); + const bool force_height_resolve = true; if (split_interval) { - // Derive per-row start phase against the selected layer pass heights so - // complementary mixed rows can coexist on the same nominal layer. - std::vector start_with_component_a = row_next_component_a; - std::vector row_seen_in_interval(mixed_rows.size(), uint8_t(0)); - if (preferred_a <= EPSILON && preferred_b <= EPSILON) { - for (size_t channel_idx = 0; channel_idx < segmentation[layer_id].size(); ++channel_idx) { - const ExPolygons &state_masks = segmentation[layer_id][channel_idx]; - if (state_masks.empty()) - continue; - - const unsigned int state_id = unsigned(channel_idx + 1); - if (!mixed_mgr.is_mixed(state_id, num_physical)) - continue; - const int mixed_idx = mixed_mgr.mixed_index_from_filament_id(state_id, num_physical); - if (mixed_idx < 0 || size_t(mixed_idx) >= mixed_rows.size()) - continue; - const MixedFilament &mf = mixed_rows[size_t(mixed_idx)]; - if (!local_z_eligible_mixed_row(mf)) - continue; - row_seen_in_interval[size_t(mixed_idx)] = uint8_t(1); - if (row_phase_initialized[size_t(mixed_idx)] != 0) - continue; - - double row_h_a = 0.0; - double row_h_b = 0.0; - compute_local_z_gradient_component_heights(mf.mix_b_percent, mixed_lower, mixed_upper, row_h_a, row_h_b); - - double err_ab = 0.0; - double err_ba = 0.0; - for (size_t pass_i = 0; pass_i < pass_heights.size(); ++pass_i) { - const double expected_ab = (pass_i % 2) == 0 ? row_h_a : row_h_b; - const double expected_ba = (pass_i % 2) == 0 ? row_h_b : row_h_a; - err_ab += std::abs(pass_heights[pass_i] - expected_ab); - err_ba += std::abs(pass_heights[pass_i] - expected_ba); - } - if (err_ba + 1e-6 < err_ab) - start_with_component_a[size_t(mixed_idx)] = uint8_t(0); - } - } - ++split_intervals; - double z_cursor = interval.z_lo; - size_t pass_idx = 0; bool interval_has_split_painted_masks = false; - interval.sublayer_height = *std::min_element(pass_heights.begin(), pass_heights.end()); - for (const double pass_height_nominal : pass_heights) { - if (z_cursor >= interval.z_hi - EPSILON) - break; - const double pass_height = std::min(pass_height_nominal, interval.z_hi - z_cursor); - const double z_next = std::min(interval.z_hi, z_cursor + pass_height); + if (isolated_multi_row_mode) { + std::vector isolated_plans; + isolated_plans.reserve(std::max(2, active_mixed_rows * 2)); - SubLayerPlan plan; - plan.layer_id = layer_id; - plan.pass_index = pass_idx; - plan.split_interval = true; - plan.z_lo = z_cursor; - plan.z_hi = z_next; - plan.print_z = z_next; - plan.flow_height = pass_height; - plan.painted_masks_by_extruder.assign(num_physical, ExPolygons()); - ++split_passes_total; - bool pass_has_painted_masks = false; - - for (size_t channel_idx = 0; channel_idx < segmentation[layer_id].size(); ++channel_idx) { - const ExPolygons &state_masks = segmentation[layer_id][channel_idx]; + for (size_t row_idx = 0; row_idx < row_active_this_layer.size(); ++row_idx) { + if (row_active_this_layer[row_idx] == 0) + continue; + const ExPolygons &state_masks = row_state_masks[row_idx]; if (state_masks.empty()) continue; - const unsigned int state_id = unsigned(channel_idx + 1); - if (!mixed_mgr.is_mixed(state_id, num_physical)) - continue; - const int mixed_idx = mixed_mgr.mixed_index_from_filament_id(state_id, num_physical); - if (mixed_idx < 0 || size_t(mixed_idx) >= mixed_rows.size()) - continue; - const MixedFilament &mf = mixed_rows[size_t(mixed_idx)]; - if (!local_z_eligible_mixed_row(mf)) - continue; - row_seen_in_interval[size_t(mixed_idx)] = uint8_t(1); - ++forced_height_resolve_calls; - unsigned int target_extruder = 0; - if (mf.component_a > 0 && mf.component_a <= num_physical && - mf.component_b > 0 && mf.component_b <= num_physical) { - const bool start_a = start_with_component_a[size_t(mixed_idx)] != 0; - const bool even_pass = (pass_idx % 2) == 0; - // Enforce strict per-pass alternation inside split local-Z intervals. - target_extruder = even_pass - ? (start_a ? mf.component_a : mf.component_b) - : (start_a ? mf.component_b : mf.component_a); - ++strict_ab_assignments; + const MixedFilament &mf = mixed_rows[row_idx]; + const std::vector &row_passes_raw = isolated_row_pass_heights[row_idx]; + const std::vector row_passes = row_passes_raw.empty() + ? std::vector{ interval.base_height } + : row_passes_raw; + const bool valid_pair = mf.component_a > 0 && mf.component_a <= num_physical && + mf.component_b > 0 && mf.component_b <= num_physical; + + bool start_with_a = true; + if (valid_pair && preferred_a <= EPSILON && preferred_b <= EPSILON) { + double row_h_a = 0.0; + double row_h_b = 0.0; + compute_local_z_gradient_component_heights(mf.mix_b_percent, mixed_lower, mixed_upper, row_h_a, row_h_b); + double err_ab = 0.0; + double err_ba = 0.0; + for (size_t pass_i = 0; pass_i < row_passes.size(); ++pass_i) { + const double expected_ab = (pass_i % 2) == 0 ? row_h_a : row_h_b; + const double expected_ba = (pass_i % 2) == 0 ? row_h_b : row_h_a; + err_ab += std::abs(row_passes[pass_i] - expected_ab); + err_ba += std::abs(row_passes[pass_i] - expected_ba); + } + if (err_ba + 1e-6 < err_ab) + start_with_a = false; } - if (target_extruder == 0) { - target_extruder = mixed_mgr.resolve(state_id, num_physical, cadence_index, float(plan.print_z), float(plan.flow_height), true); + + double z_cursor = interval.z_lo; + for (size_t pass_i = 0; pass_i < row_passes.size(); ++pass_i) { + if (z_cursor >= interval.z_hi - EPSILON) + break; + + const double pass_height = std::min(row_passes[pass_i], interval.z_hi - z_cursor); + if (pass_height <= EPSILON) + continue; + const double z_next = std::min(interval.z_hi, z_cursor + pass_height); + + SubLayerPlan plan; + plan.layer_id = layer_id; + plan.pass_index = isolated_plans.size(); + plan.split_interval = true; + plan.z_lo = z_cursor; + plan.z_hi = z_next; + plan.print_z = z_next; + plan.flow_height = pass_height; + plan.painted_masks_by_extruder.assign(num_physical, ExPolygons()); + ++split_passes_total; + ++forced_height_resolve_calls; + + unsigned int target_extruder = 0; + if (valid_pair) { + const bool even_pass = (pass_i % 2) == 0; + target_extruder = even_pass + ? (start_with_a ? mf.component_a : mf.component_b) + : (start_with_a ? mf.component_b : mf.component_a); + ++strict_ab_assignments; + } + if (target_extruder == 0) { + const unsigned int state_id = row_state_ids[row_idx]; + if (state_id != 0) { + target_extruder = mixed_mgr.resolve(state_id, + num_physical, + row_cadence_index[row_idx], + float(plan.print_z), + float(plan.flow_height), + force_height_resolve); + } + } + if (target_extruder == 0 || target_extruder > num_physical) { + ++forced_height_resolve_invalid_target; + } else { + append(plan.painted_masks_by_extruder[target_extruder - 1], state_masks); + ++split_passes_with_painted_masks; + interval_has_split_painted_masks = true; + } + + isolated_plans.emplace_back(std::move(plan)); + ++row_cadence_index[row_idx]; + z_cursor = z_next; } - if (target_extruder == 0 || target_extruder > num_physical) { - ++forced_height_resolve_invalid_target; - continue; - } - append(plan.painted_masks_by_extruder[target_extruder - 1], state_masks); - pass_has_painted_masks = true; - } - for (ExPolygons &masks : plan.painted_masks_by_extruder) - if (masks.size() > 1) - masks = union_ex(masks); - if (pass_has_painted_masks) { - ++split_passes_with_painted_masks; - interval_has_split_painted_masks = true; } - if (z_next >= interval.z_hi - EPSILON) - plan.base_masks = base_masks; + if (!isolated_plans.empty()) { + std::sort(isolated_plans.begin(), isolated_plans.end(), [](const SubLayerPlan &lhs, const SubLayerPlan &rhs) { + if (std::abs(lhs.print_z - rhs.print_z) > EPSILON) + return lhs.print_z < rhs.print_z; + if (std::abs(lhs.z_lo - rhs.z_lo) > EPSILON) + return lhs.z_lo < rhs.z_lo; + return lhs.pass_index < rhs.pass_index; + }); + double min_flow_height = isolated_plans.front().flow_height; + double max_flow_height = isolated_plans.front().flow_height; + for (size_t idx = 0; idx < isolated_plans.size(); ++idx) { + isolated_plans[idx].pass_index = idx; + min_flow_height = std::min(min_flow_height, isolated_plans[idx].flow_height); + max_flow_height = std::max(max_flow_height, isolated_plans[idx].flow_height); + } + isolated_plans.back().base_masks = base_masks; + interval.sublayer_height = min_flow_height; + pass_count_for_log = isolated_plans.size(); + pass_min_height_for_log = min_flow_height; + pass_max_height_for_log = max_flow_height; + for (SubLayerPlan &plan : isolated_plans) { + plans.emplace_back(std::move(plan)); + ++interval.sublayer_count; + ++total_generated_sublayer_cnt; + } + } + } else { + // Derive per-row orientation against pass heights so each mixed row + // maps thicker/thinner subpasses to the intended component. + std::vector start_with_component_a(mixed_rows.size(), uint8_t(1)); + if (preferred_a <= EPSILON && preferred_b <= EPSILON) { + for (size_t channel_idx = 0; channel_idx < segmentation[layer_id].size(); ++channel_idx) { + const ExPolygons &state_masks = segmentation[layer_id][channel_idx]; + if (state_masks.empty()) + continue; - plans.emplace_back(std::move(plan)); - ++interval.sublayer_count; - ++total_generated_sublayer_cnt; - ++pass_idx; - ++cadence_index; - z_cursor = z_next; - } + const unsigned int state_id = unsigned(channel_idx + 1); + if (!mixed_mgr.is_mixed(state_id, num_physical)) + continue; + const int mixed_idx = mixed_mgr.mixed_index_from_filament_id(state_id, num_physical); + if (mixed_idx < 0 || size_t(mixed_idx) >= mixed_rows.size()) + continue; + const MixedFilament &mf = mixed_rows[size_t(mixed_idx)]; + if (!local_z_eligible_mixed_row(mf)) + continue; - if (pass_idx > 0) { - const bool toggle_after_interval = (pass_idx % 2) != 0; - for (size_t mixed_idx = 0; mixed_idx < mixed_rows.size(); ++mixed_idx) { - if (row_seen_in_interval[mixed_idx] == 0) - continue; - const bool start_a = start_with_component_a[mixed_idx] != 0; - const bool next_a = toggle_after_interval ? !start_a : start_a; - row_next_component_a[mixed_idx] = next_a ? uint8_t(1) : uint8_t(0); - row_phase_initialized[mixed_idx] = uint8_t(1); + double row_h_a = 0.0; + double row_h_b = 0.0; + compute_local_z_gradient_component_heights(mf.mix_b_percent, mixed_lower, mixed_upper, row_h_a, row_h_b); + + double err_ab = 0.0; + double err_ba = 0.0; + for (size_t pass_i = 0; pass_i < pass_heights.size(); ++pass_i) { + const double expected_ab = (pass_i % 2) == 0 ? row_h_a : row_h_b; + const double expected_ba = (pass_i % 2) == 0 ? row_h_b : row_h_a; + err_ab += std::abs(pass_heights[pass_i] - expected_ab); + err_ba += std::abs(pass_heights[pass_i] - expected_ba); + } + if (err_ba + 1e-6 < err_ab) + start_with_component_a[size_t(mixed_idx)] = uint8_t(0); + } + } + + double z_cursor = interval.z_lo; + size_t pass_idx = 0; + interval.sublayer_height = *std::min_element(pass_heights.begin(), pass_heights.end()); + for (const double pass_height_nominal : pass_heights) { + if (z_cursor >= interval.z_hi - EPSILON) + break; + const double pass_height = std::min(pass_height_nominal, interval.z_hi - z_cursor); + const double z_next = std::min(interval.z_hi, z_cursor + pass_height); + + SubLayerPlan plan; + plan.layer_id = layer_id; + plan.pass_index = pass_idx; + plan.split_interval = true; + plan.z_lo = z_cursor; + plan.z_hi = z_next; + plan.print_z = z_next; + plan.flow_height = pass_height; + plan.painted_masks_by_extruder.assign(num_physical, ExPolygons()); + ++split_passes_total; + bool pass_has_painted_masks = false; + std::vector row_seen_in_pass(mixed_rows.size(), uint8_t(0)); + + for (size_t channel_idx = 0; channel_idx < segmentation[layer_id].size(); ++channel_idx) { + const ExPolygons &state_masks = segmentation[layer_id][channel_idx]; + if (state_masks.empty()) + continue; + + const unsigned int state_id = unsigned(channel_idx + 1); + if (!mixed_mgr.is_mixed(state_id, num_physical)) + continue; + const int mixed_idx = mixed_mgr.mixed_index_from_filament_id(state_id, num_physical); + if (mixed_idx < 0 || size_t(mixed_idx) >= mixed_rows.size()) + continue; + const size_t row_idx = size_t(mixed_idx); + const MixedFilament &mf = mixed_rows[row_idx]; + if (!local_z_eligible_mixed_row(mf)) + continue; + row_seen_in_pass[row_idx] = uint8_t(1); + ++forced_height_resolve_calls; + unsigned int target_extruder = 0; + if (mf.component_a > 0 && mf.component_a <= num_physical && + mf.component_b > 0 && mf.component_b <= num_physical) { + const bool start_a = start_with_component_a[row_idx] != 0; + const bool even_pass = (pass_idx % 2) == 0; + // Local-Z mode alternates A/B on every subpass. + target_extruder = even_pass + ? (start_a ? mf.component_a : mf.component_b) + : (start_a ? mf.component_b : mf.component_a); + ++strict_ab_assignments; + } + if (target_extruder == 0) { + target_extruder = mixed_mgr.resolve(state_id, + num_physical, + row_cadence_index[row_idx], + float(plan.print_z), + float(plan.flow_height), + force_height_resolve); + } + if (target_extruder == 0 || target_extruder > num_physical) { + ++forced_height_resolve_invalid_target; + continue; + } + append(plan.painted_masks_by_extruder[target_extruder - 1], state_masks); + pass_has_painted_masks = true; + } + for (ExPolygons &masks : plan.painted_masks_by_extruder) + if (masks.size() > 1) + masks = union_ex(masks); + if (pass_has_painted_masks) { + ++split_passes_with_painted_masks; + interval_has_split_painted_masks = true; + } + + if (z_next >= interval.z_hi - EPSILON) + plan.base_masks = base_masks; + + plans.emplace_back(std::move(plan)); + ++interval.sublayer_count; + ++total_generated_sublayer_cnt; + ++pass_idx; + for (size_t mixed_idx = 0; mixed_idx < row_seen_in_pass.size(); ++mixed_idx) + if (row_seen_in_pass[mixed_idx] != 0) + ++row_cadence_index[mixed_idx]; + z_cursor = z_next; } } if (!interval_has_split_painted_masks) @@ -2141,6 +2459,7 @@ static void build_local_z_plan(PrintObject &print_object, const std::vector row_seen_in_interval(mixed_rows.size(), uint8_t(0)); for (size_t channel_idx = 0; channel_idx < segmentation[layer_id].size(); ++channel_idx) { const ExPolygons &state_masks = segmentation[layer_id][channel_idx]; @@ -2150,12 +2469,22 @@ static void build_local_z_plan(PrintObject &print_object, const std::vector= mixed_rows.size()) continue; + const size_t row_idx = size_t(mixed_idx); + const MixedFilament &mixed_row = mixed_rows[row_idx]; + if (!local_z_eligible_mixed_row(mixed_row)) + continue; + row_seen_in_interval[row_idx] = uint8_t(1); ++forced_height_resolve_calls; const unsigned int target_extruder = - mixed_mgr.resolve(state_id, num_physical, cadence_index, float(plan.print_z), float(plan.flow_height), true); + mixed_mgr.resolve(state_id, + num_physical, + row_cadence_index[row_idx], + float(plan.print_z), + float(plan.flow_height), + force_height_resolve); if (target_extruder == 0 || target_extruder > num_physical) { ++forced_height_resolve_invalid_target; continue; @@ -2169,7 +2498,9 @@ static void build_local_z_plan(PrintObject &print_object, const std::vector(wxGetApp().mainframe), + wxID_ANY, + wxString::Format(_L("About %s"), _L("Snapmaker Orca Full Spectrum")), + wxDefaultPosition, + wxDefaultSize, + /*wxCAPTION*/wxDEFAULT_DIALOG_STYLE) { SetFont(wxGetApp().normal_font()); SetBackgroundColour(*wxWHITE); @@ -242,7 +246,7 @@ AboutDialog::AboutDialog() // version { vesizer->Add(0, FromDIP(165), 1, wxEXPAND, FromDIP(5)); - auto version_string = _L("Snapmaker Orca ") + " " + std::string(Snapmaker_VERSION); + auto version_string = _L("Snapmaker Orca Full Spectrum") + " " + std::string(Snapmaker_VERSION); wxStaticText* version = new wxStaticText(this, wxID_ANY, version_string.c_str(), wxDefaultPosition, wxDefaultSize); wxStaticText* bs_version = new wxStaticText(this, wxID_ANY, wxString::Format("Based on Orca Slicer"), wxDefaultPosition, wxDefaultSize); bs_version->SetFont(Label::Body_12); @@ -350,7 +354,11 @@ AboutDialog::AboutDialog() (boost::format( "" "" - "

www.snapmaker.com

" + "

" + "www.snapmaker.com" + "  |  " + "GitHub" + "

" "" "") ).str());