mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-10 02:37:04 +00:00
Enhance color blending in MixedFilament: Introduce RYB pigment-style blending for improved color mixing accuracy. Add RGB to RYB and RYB to RGB conversion functions, and update blend_color method to utilize the new blending approach. Improve error handling in hex color parsing.
This commit is contained in:
223
docs/U1_Local_Z_Dithering_Design_Draft.md
Normal file
223
docs/U1_Local_Z_Dithering_Design_Draft.md
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
# U1 Local Z Dithering Design Draft
|
||||||
|
|
||||||
|
Status: Draft for later implementation
|
||||||
|
Last updated: 2026-02-10
|
||||||
|
Owner: Snapmaker Orca engineering
|
||||||
|
|
||||||
|
## 1. Problem Statement
|
||||||
|
|
||||||
|
Current dithering support can alternate component filaments in Z, but layer height is still resolved globally per object layer.
|
||||||
|
This means:
|
||||||
|
|
||||||
|
- `dithering_z_step_size` can only change full layers (or full Z bands), not only painted XY zones.
|
||||||
|
- The requested behavior (`---===`) is not achievable today:
|
||||||
|
- non-painted area keeps base height (example: `0.12`)
|
||||||
|
- painted mixed area is subdivided (example: `0.06 + 0.06`) in the same nominal Z interval
|
||||||
|
|
||||||
|
## 2. Current Architecture Constraints
|
||||||
|
|
||||||
|
Key code paths:
|
||||||
|
|
||||||
|
- Layer heights are created before segmentation:
|
||||||
|
- `src/libslic3r/PrintObjectSlice.cpp` (`update_layer_height_profile`, `generate_object_layers`)
|
||||||
|
- Mixed painting segmentation is applied after layers already exist:
|
||||||
|
- `src/libslic3r/PrintObjectSlice.cpp` (`apply_mm_segmentation`)
|
||||||
|
- `src/libslic3r/MultiMaterialSegmentation.cpp`
|
||||||
|
- `Layer` has one `height`, `slice_z`, `print_z` for the entire layer:
|
||||||
|
- `src/libslic3r/Layer.hpp`
|
||||||
|
|
||||||
|
Conclusion: existing pipeline assumes one global Z step per layer. Local per-XY sublayering needs a new planning model.
|
||||||
|
|
||||||
|
## 3. Goals
|
||||||
|
|
||||||
|
- Support local Z subdivision for mixed-painted zones only.
|
||||||
|
- Keep base regions at user base layer height whenever possible.
|
||||||
|
- Preserve current mixed filament alternation logic (A/B cadence).
|
||||||
|
- Keep existing behavior when local mode is disabled.
|
||||||
|
- Avoid regressions in non-mixed prints.
|
||||||
|
|
||||||
|
## 4. Non-Goals (Phase 1)
|
||||||
|
|
||||||
|
- Full non-planar slicing.
|
||||||
|
- Arbitrary local adaptive mesh refinement outside mixed-painted zones.
|
||||||
|
- Rewriting all perimeter/infill algorithms from scratch.
|
||||||
|
|
||||||
|
## 5. Proposed Feature Model
|
||||||
|
|
||||||
|
Add a new mode on top of current dithering:
|
||||||
|
|
||||||
|
- `dithering_local_z_mode` (bool, default `false`)
|
||||||
|
- Existing `dithering_z_step_size` remains the micro step for painted zones.
|
||||||
|
- Existing `dithering_step_painted_zones_only` remains as compatibility switch for current global mode.
|
||||||
|
|
||||||
|
When `dithering_local_z_mode = true`:
|
||||||
|
|
||||||
|
- For each base Z interval `[z0, z1]`:
|
||||||
|
- if no mixed paint intersects interval: print normally at base layer height.
|
||||||
|
- if mixed paint intersects interval: split interval into sublayers at `dithering_z_step_size`.
|
||||||
|
- mixed-painted XY polygons are printed on each sublayer with alternating components.
|
||||||
|
- non-mixed XY polygons are printed as a base-height pass in that interval (not duplicated every sublayer).
|
||||||
|
|
||||||
|
## 6. High-Level Architecture
|
||||||
|
|
||||||
|
Introduce a two-level planning pipeline:
|
||||||
|
|
||||||
|
1. Base Layer Plan (existing):
|
||||||
|
- Build base object layers as today.
|
||||||
|
2. Local Z Expansion Plan (new):
|
||||||
|
- For base layers that intersect mixed-painted areas, build `SubLayerPlan` entries.
|
||||||
|
3. Toolpath Assignment Plan (new):
|
||||||
|
- Route painted polygons to sublayers.
|
||||||
|
- Route non-painted polygons to one base-height pass within same interval.
|
||||||
|
4. G-code Scheduler (extended):
|
||||||
|
- Emit sublayer passes in Z order while respecting extrusion height per pass.
|
||||||
|
|
||||||
|
## 7. New Data Structures (Draft)
|
||||||
|
|
||||||
|
Add new planning structs (names tentative):
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
struct LocalZInterval {
|
||||||
|
double z_lo;
|
||||||
|
double z_hi;
|
||||||
|
double base_height; // e.g. 0.12
|
||||||
|
double sublayer_height; // e.g. 0.06
|
||||||
|
bool has_mixed_paint;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SubLayerPlan {
|
||||||
|
double z_lo;
|
||||||
|
double z_hi;
|
||||||
|
double print_z;
|
||||||
|
double flow_height;
|
||||||
|
// per extruder painted masks for this sublayer
|
||||||
|
std::vector<ExPolygons> painted_masks_by_extruder;
|
||||||
|
// polygons printed as normal/non-mixed in this pass
|
||||||
|
ExPolygons base_masks;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Attach `std::vector<SubLayerPlan>` to `PrintObject` (or a dedicated planner cache) without immediately replacing `Layer`.
|
||||||
|
|
||||||
|
## 8. Algorithm Draft
|
||||||
|
|
||||||
|
### 8.1 Build Local Z Intervals
|
||||||
|
|
||||||
|
- Start from base layer intervals from `generate_object_layers`.
|
||||||
|
- For each base interval, query whether mixed-painted states are present in that Z range.
|
||||||
|
- If not present, interval remains single-pass.
|
||||||
|
- If present, split into `N = ceil(base_height / z_step)` subintervals.
|
||||||
|
|
||||||
|
### 8.2 Build Painted Masks Per SubLayer
|
||||||
|
|
||||||
|
- Re-run or adapt `multi_material_segmentation_by_painting` to produce painted masks at sublayer Z samples (not only base layer Z samples).
|
||||||
|
- For each sublayer:
|
||||||
|
- derive active mixed pair (A/B) based on cadence index.
|
||||||
|
- assign painted XY polygons to active physical extruder for that sublayer.
|
||||||
|
|
||||||
|
### 8.3 Preserve Base Regions at Base Height
|
||||||
|
|
||||||
|
- For non-painted polygons inside a locally-split base interval:
|
||||||
|
- emit once with `flow_height = base_height` in a designated pass (typically final sublayer pass of interval).
|
||||||
|
- do not emit these polygons on intermediate sublayers.
|
||||||
|
|
||||||
|
### 8.4 Boundary Handling
|
||||||
|
|
||||||
|
- At boundaries between painted and non-painted masks:
|
||||||
|
- enforce overlap/tolerance compensation to avoid cracks.
|
||||||
|
- clip with robust polygon booleans and min-area filtering.
|
||||||
|
- Add seam strategy notes for transitions (TBD in implementation).
|
||||||
|
|
||||||
|
## 9. Required Code Areas
|
||||||
|
|
||||||
|
Primary:
|
||||||
|
|
||||||
|
- `src/libslic3r/PrintObjectSlice.cpp` (new local-Z planning stage, call ordering)
|
||||||
|
- `src/libslic3r/MultiMaterialSegmentation.cpp` (sublayer mask generation)
|
||||||
|
- `src/libslic3r/PrintObject.cpp` (cache invalidation + storage for local-Z plans)
|
||||||
|
- `src/libslic3r/GCode/*` (scheduler/tool ordering to emit sublayer passes correctly)
|
||||||
|
|
||||||
|
Likely:
|
||||||
|
|
||||||
|
- `src/libslic3r/Layer*` (if promoting local-z plan into first-class layer abstraction)
|
||||||
|
- `src/libslic3r/PrintApply.cpp` (config propagation and reset handling)
|
||||||
|
- `src/slic3r/GUI/Tab.cpp` and `src/libslic3r/PrintConfig.*` (new option + tooltips)
|
||||||
|
|
||||||
|
## 10. Compatibility and Migration
|
||||||
|
|
||||||
|
- Keep existing `dithering_z_step_size` behavior when `dithering_local_z_mode=false`.
|
||||||
|
- Hide/disable local-Z checkbox unless mixed virtual filament is enabled.
|
||||||
|
- Project files without new key must load as current behavior.
|
||||||
|
- Fallback: if local-Z planner fails, auto-fallback to current global mode with warning.
|
||||||
|
|
||||||
|
## 11. Validation Plan
|
||||||
|
|
||||||
|
### Unit/logic tests
|
||||||
|
|
||||||
|
- Interval splitting:
|
||||||
|
- no mixed paint -> no split
|
||||||
|
- mixed paint -> expected number of sublayers
|
||||||
|
- Cadence:
|
||||||
|
- A/B alternation across sublayers matches configured ratio/step
|
||||||
|
- Config changes:
|
||||||
|
- step size changes between slices must invalidate planner cache
|
||||||
|
|
||||||
|
### Integration tests
|
||||||
|
|
||||||
|
- Painted stripe through object:
|
||||||
|
- verify non-painted region keeps base pass count
|
||||||
|
- verify painted region gets subdivided passes
|
||||||
|
- Multi-object plate with only one mixed object.
|
||||||
|
- Support + mixed paint interaction.
|
||||||
|
|
||||||
|
### G-code assertions
|
||||||
|
|
||||||
|
- In affected intervals:
|
||||||
|
- expected `Z` move cadence in mixed zones
|
||||||
|
- base region extrusion appears once per base interval
|
||||||
|
- In unaffected intervals:
|
||||||
|
- unchanged base layer Z cadence
|
||||||
|
|
||||||
|
## 12. Risks
|
||||||
|
|
||||||
|
- High complexity around perimeter/fill continuity at mask boundaries.
|
||||||
|
- Performance impact from finer slicing and extra polygon clipping.
|
||||||
|
- Increased memory use for per-sublayer painted masks.
|
||||||
|
- Potential regressions in wipe tower/tool ordering and support synchronization.
|
||||||
|
|
||||||
|
## 13. Rollout Plan
|
||||||
|
|
||||||
|
Phase A - Planner skeleton (no G-code changes yet):
|
||||||
|
|
||||||
|
- Build and debug `LocalZInterval` and `SubLayerPlan`.
|
||||||
|
- Add debug export (SVG/JSON) for visual verification.
|
||||||
|
|
||||||
|
Phase B - Limited emission path:
|
||||||
|
|
||||||
|
- Enable local-Z only for perimeter walls on painted regions.
|
||||||
|
- Keep infill/base regions on current behavior for early validation.
|
||||||
|
|
||||||
|
Phase C - Full emission:
|
||||||
|
|
||||||
|
- Perimeters + infill + top/bottom in local-Z path.
|
||||||
|
- Boundary compensation and seam cleanup.
|
||||||
|
|
||||||
|
Phase D - Stabilization:
|
||||||
|
|
||||||
|
- Performance tuning, cache policy, presets/profile updates.
|
||||||
|
- Expand regression suite and add fixture models.
|
||||||
|
|
||||||
|
## 14. Open Questions
|
||||||
|
|
||||||
|
- Should non-painted regions in split intervals be printed on first or last sublayer pass?
|
||||||
|
- Do we allow base-height extrusion in an interval where painted sublayers already deposited material nearby, or enforce sublayer-only flow there?
|
||||||
|
- How should top/bottom skin logic behave when only part of a layer is sublayered?
|
||||||
|
- Should local-Z mode require a minimum nozzle/step ratio gate for print safety?
|
||||||
|
|
||||||
|
## 15. Recommended Next Step
|
||||||
|
|
||||||
|
Prototype only Phase A:
|
||||||
|
|
||||||
|
- Build `LocalZInterval`/`SubLayerPlan` and dump debug artifacts.
|
||||||
|
- No toolpath emission changes yet.
|
||||||
|
- Use this to validate that painted masks and interval splitting are stable before committing to full pipeline rewrite.
|
||||||
@@ -16,14 +16,125 @@ struct RGB {
|
|||||||
int r = 0, g = 0, b = 0;
|
int r = 0, g = 0, b = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct RGBf {
|
||||||
|
float r = 0.f, g = 0.f, b = 0.f;
|
||||||
|
};
|
||||||
|
|
||||||
|
static float clamp01(float v)
|
||||||
|
{
|
||||||
|
return std::max(0.f, std::min(1.f, v));
|
||||||
|
}
|
||||||
|
|
||||||
|
static RGBf to_rgbf(const RGB &c)
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
clamp01(static_cast<float>(c.r) / 255.f),
|
||||||
|
clamp01(static_cast<float>(c.g) / 255.f),
|
||||||
|
clamp01(static_cast<float>(c.b) / 255.f)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static RGB to_rgb8(const RGBf &c)
|
||||||
|
{
|
||||||
|
auto to_u8 = [](float v) -> int {
|
||||||
|
return std::clamp(static_cast<int>(std::round(clamp01(v) * 255.f)), 0, 255);
|
||||||
|
};
|
||||||
|
return { to_u8(c.r), to_u8(c.g), to_u8(c.b) };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert RGB to an artist-pigment style RYB space.
|
||||||
|
// This is an approximation, but it gives expected pair mixes:
|
||||||
|
// Red + Blue -> Purple, Blue + Yellow -> Green, Red + Yellow -> Orange.
|
||||||
|
static RGBf rgb_to_ryb(RGBf in)
|
||||||
|
{
|
||||||
|
float r = clamp01(in.r);
|
||||||
|
float g = clamp01(in.g);
|
||||||
|
float b = clamp01(in.b);
|
||||||
|
|
||||||
|
const float white = std::min({ r, g, b });
|
||||||
|
r -= white;
|
||||||
|
g -= white;
|
||||||
|
b -= white;
|
||||||
|
|
||||||
|
const float max_g = std::max({ r, g, b });
|
||||||
|
|
||||||
|
float y = std::min(r, g);
|
||||||
|
r -= y;
|
||||||
|
g -= y;
|
||||||
|
|
||||||
|
if (b > 0.f && g > 0.f) {
|
||||||
|
b *= 0.5f;
|
||||||
|
g *= 0.5f;
|
||||||
|
}
|
||||||
|
|
||||||
|
y += g;
|
||||||
|
b += g;
|
||||||
|
|
||||||
|
const float max_y = std::max({ r, y, b });
|
||||||
|
if (max_y > 1e-6f) {
|
||||||
|
const float n = max_g / max_y;
|
||||||
|
r *= n;
|
||||||
|
y *= n;
|
||||||
|
b *= n;
|
||||||
|
}
|
||||||
|
|
||||||
|
r += white;
|
||||||
|
y += white;
|
||||||
|
b += white;
|
||||||
|
return { clamp01(r), clamp01(y), clamp01(b) };
|
||||||
|
}
|
||||||
|
|
||||||
|
static RGBf ryb_to_rgb(RGBf in)
|
||||||
|
{
|
||||||
|
float r = clamp01(in.r);
|
||||||
|
float y = clamp01(in.g);
|
||||||
|
float b = clamp01(in.b);
|
||||||
|
|
||||||
|
const float white = std::min({ r, y, b });
|
||||||
|
r -= white;
|
||||||
|
y -= white;
|
||||||
|
b -= white;
|
||||||
|
|
||||||
|
const float max_y = std::max({ r, y, b });
|
||||||
|
|
||||||
|
float g = std::min(y, b);
|
||||||
|
y -= g;
|
||||||
|
b -= g;
|
||||||
|
|
||||||
|
if (b > 0.f && g > 0.f) {
|
||||||
|
b *= 2.f;
|
||||||
|
g *= 2.f;
|
||||||
|
}
|
||||||
|
|
||||||
|
r += y;
|
||||||
|
g += y;
|
||||||
|
|
||||||
|
const float max_g = std::max({ r, g, b });
|
||||||
|
if (max_g > 1e-6f) {
|
||||||
|
const float n = max_y / max_g;
|
||||||
|
r *= n;
|
||||||
|
g *= n;
|
||||||
|
b *= n;
|
||||||
|
}
|
||||||
|
|
||||||
|
r += white;
|
||||||
|
g += white;
|
||||||
|
b += white;
|
||||||
|
return { clamp01(r), clamp01(g), clamp01(b) };
|
||||||
|
}
|
||||||
|
|
||||||
// Parse "#RRGGBB" to RGB. Returns black on failure.
|
// Parse "#RRGGBB" to RGB. Returns black on failure.
|
||||||
static RGB parse_hex_color(const std::string &hex)
|
static RGB parse_hex_color(const std::string &hex)
|
||||||
{
|
{
|
||||||
RGB c;
|
RGB c;
|
||||||
if (hex.size() >= 7 && hex[0] == '#') {
|
if (hex.size() >= 7 && hex[0] == '#') {
|
||||||
c.r = std::stoi(hex.substr(1, 2), nullptr, 16);
|
try {
|
||||||
c.g = std::stoi(hex.substr(3, 2), nullptr, 16);
|
c.r = std::stoi(hex.substr(1, 2), nullptr, 16);
|
||||||
c.b = std::stoi(hex.substr(5, 2), nullptr, 16);
|
c.g = std::stoi(hex.substr(3, 2), nullptr, 16);
|
||||||
|
c.b = std::stoi(hex.substr(5, 2), nullptr, 16);
|
||||||
|
} catch (...) {
|
||||||
|
c = {};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
@@ -124,38 +235,34 @@ std::string MixedFilamentManager::blend_color(const std::string &color_a,
|
|||||||
const std::string &color_b,
|
const std::string &color_b,
|
||||||
int ratio_a, int ratio_b)
|
int ratio_a, int ratio_b)
|
||||||
{
|
{
|
||||||
RGB a = parse_hex_color(color_a);
|
const int safe_a = std::max(0, ratio_a);
|
||||||
RGB b = parse_hex_color(color_b);
|
const int safe_b = std::max(0, ratio_b);
|
||||||
|
const float total = static_cast<float>(safe_a + safe_b);
|
||||||
// Additive blend: min(a + b, 255) per channel.
|
const float wa = (total > 0.f) ? static_cast<float>(safe_a) / total : 0.5f;
|
||||||
// For unequal ratios, weight accordingly.
|
|
||||||
const float total = static_cast<float>(ratio_a + ratio_b);
|
|
||||||
const float wa = (total > 0.f) ? static_cast<float>(ratio_a) / total : 0.5f;
|
|
||||||
const float wb = 1.f - wa;
|
const float wb = 1.f - wa;
|
||||||
|
|
||||||
// Use screen blending which is additive-like without oversaturation:
|
const RGBf rgb_a = to_rgbf(parse_hex_color(color_a));
|
||||||
// screen(A, B) = A + B - A*B/255
|
const RGBf rgb_b = to_rgbf(parse_hex_color(color_b));
|
||||||
// Weighted variant: blend each channel independently.
|
const RGBf ryb_a = rgb_to_ryb(rgb_a);
|
||||||
auto screen_ch = [](int ca, int cb, float wa, float wb) -> int {
|
const RGBf ryb_b = rgb_to_ryb(rgb_b);
|
||||||
// Weighted additive with clamping – matches user expectation:
|
|
||||||
// Red(255,0,0) + Green(0,255,0) = Yellow(255,255,0)
|
|
||||||
float v = static_cast<float>(ca) * wa + static_cast<float>(cb) * wb;
|
|
||||||
// Boost towards additive: add the minimum so pure colours combine fully.
|
|
||||||
float additive = std::min(static_cast<float>(ca + cb), 255.f);
|
|
||||||
// Blend between weighted-average and full-additive based on colour distance.
|
|
||||||
float result = wa * static_cast<float>(ca) + wb * static_cast<float>(cb);
|
|
||||||
// For the 1:1 case, use pure additive (clamped) to get R+G=Y.
|
|
||||||
if (std::abs(wa - wb) < 0.01f)
|
|
||||||
result = additive;
|
|
||||||
return std::min(static_cast<int>(std::round(result)), 255);
|
|
||||||
};
|
|
||||||
|
|
||||||
RGB out;
|
RGBf ryb_out;
|
||||||
out.r = screen_ch(a.r, b.r, wa, wb);
|
ryb_out.r = wa * ryb_a.r + wb * ryb_b.r;
|
||||||
out.g = screen_ch(a.g, b.g, wa, wb);
|
ryb_out.g = wa * ryb_a.g + wb * ryb_b.g;
|
||||||
out.b = screen_ch(a.b, b.b, wa, wb);
|
ryb_out.b = wa * ryb_a.b + wb * ryb_b.b;
|
||||||
|
|
||||||
return rgb_to_hex(out);
|
RGBf rgb_out = ryb_to_rgb(ryb_out);
|
||||||
|
const float v_out = std::max({ rgb_out.r, rgb_out.g, rgb_out.b });
|
||||||
|
const float v_tgt = wa * std::max({ rgb_a.r, rgb_a.g, rgb_a.b }) +
|
||||||
|
wb * std::max({ rgb_b.r, rgb_b.g, rgb_b.b });
|
||||||
|
if (v_out > 1e-6f && v_tgt > 0.f) {
|
||||||
|
const float scale = v_tgt / v_out;
|
||||||
|
rgb_out.r = clamp01(rgb_out.r * scale);
|
||||||
|
rgb_out.g = clamp01(rgb_out.g * scale);
|
||||||
|
rgb_out.b = clamp01(rgb_out.b * scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rgb_to_hex(to_rgb8(rgb_out));
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t MixedFilamentManager::enabled_count() const
|
size_t MixedFilamentManager::enabled_count() const
|
||||||
@@ -177,3 +284,4 @@ std::vector<std::string> MixedFilamentManager::display_colors() const
|
|||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Slic3r
|
} // namespace Slic3r
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,9 @@
|
|||||||
namespace Slic3r {
|
namespace Slic3r {
|
||||||
|
|
||||||
// Represents a virtual "mixed" filament created by alternating layers of two
|
// Represents a virtual "mixed" filament created by alternating layers of two
|
||||||
// physical filaments. The display colour is an additive RGB blend so that,
|
// physical filaments. The display colour uses an RYB pigment-style blend so
|
||||||
// for example, Red + Green previews as Yellow.
|
// pair previews better match expected print mixing (for example Blue+Yellow
|
||||||
|
// -> Green, Red+Yellow -> Orange, Red+Blue -> Purple).
|
||||||
struct MixedFilament
|
struct MixedFilament
|
||||||
{
|
{
|
||||||
// 1-based physical filament IDs that are combined.
|
// 1-based physical filament IDs that are combined.
|
||||||
@@ -79,8 +80,7 @@ public:
|
|||||||
// mixed filament.
|
// mixed filament.
|
||||||
unsigned int resolve(unsigned int filament_id, size_t num_physical, int layer_index) const;
|
unsigned int resolve(unsigned int filament_id, size_t num_physical, int layer_index) const;
|
||||||
|
|
||||||
// Compute a display colour by additively blending the two component
|
// Compute a display colour by blending in RYB pigment space.
|
||||||
// colours. `filament_colours` contains the physical colours only.
|
|
||||||
static std::string blend_color(const std::string &color_a,
|
static std::string blend_color(const std::string &color_a,
|
||||||
const std::string &color_b,
|
const std::string &color_b,
|
||||||
int ratio_a, int ratio_b);
|
int ratio_a, int ratio_b);
|
||||||
|
|||||||
Reference in New Issue
Block a user