From bd8b7ced6a3b70872ab914faac4904adf40d2380 Mon Sep 17 00:00:00 2001 From: Clifford Garwood Date: Mon, 28 Sep 2026 17:31:27 -0400 Subject: [PATCH] Colour the carriage markers by filament where the preview does Multi-material previews now open in Filament view, where toolpaths are coloured by the filament printing them, and a marker carrying a fixed palette colour can sit on toolpaths of that same colour. The plan already resolves which physical head each carriage is, so in that view every carriage - the primary marker included - takes its head's filament colour, from the accessor the ghosts restamp themselves with, and marker, ghost and toolpath agree on what is loaded where. The primary marker is the one every preview draws, so it goes back to its own colour when no parallel mode is active. Every other view keeps the Okabe-Ito palette, which is what identifies the carriages when colour means something else, and so does a head whose filament does not resolve. Each head is resolved once per frame and shared by its marker and its toolhead box, the way the ghosts already hoist the map they all read. Co-Authored-By: Claude Opus 5 (1M context) --- src/slic3r/GUI/GCodeViewer.cpp | 73 +++++++++++++++++++++++++++++----- src/slic3r/GUI/GCodeViewer.hpp | 4 ++ 2 files changed, 68 insertions(+), 9 deletions(-) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 9c6d956c7b..501e90edc7 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -267,7 +267,7 @@ void GCodeViewer::SequentialView::Marker::init(std::string filename) } else { m_model.init_from_file(filename); } - m_model.set_color({ 1.0f, 1.0f, 1.0f, 0.5f }); + m_model.set_color(default_color()); } //BBS: GUI refactor: add canvas size from parameters @@ -1848,6 +1848,7 @@ GCodeViewer::ImexMarkerPlan GCodeViewer::resolve_imex_marker_plan(const DynamicP else if (sc.y() < pri_center.y()) { plan.pri_box_offset_y = -plan.box_wy; } } + plan.pri_head = pri_tool; plan.carriages.reserve(sec_count); for (int i = 0; i < sec_count; ++i) { const int sec_tool = sec_tool_ids[i]; @@ -1876,6 +1877,7 @@ GCodeViewer::ImexMarkerPlan GCodeViewer::resolve_imex_marker_plan(const DynamicP // single term applied to the live primary position each frame: // `term - pos` when mirrored, `pos + term` when tracked. ImexMarkerPlan::Carriage carriage; + carriage.phys_head = sec_tool; carriage.mirror_x = is_mirror && !cross_gantry; carriage.mirror_y = is_mirror && cross_gantry; carriage.x_term = carriage.mirror_x ? (float)(sec_center.x() + pri_center.x()) @@ -1939,6 +1941,9 @@ void GCodeViewer::render_scene(int canvas_width, int canvas_height) // carriage_box_draws is populated for toolhead footprint rendering after render_marker(). // Okabe-Ito colorblind-safe palette — excludes orange (#E69F00) and sky blue (#56B4E9) // which are used by the bed zone fills, ensuring the markers contrast against the background. + // It identifies the carriages in the views that colour by anything but filament; in Filament + // view a carriage takes its own head's filament colour instead, so a marker, the ghost of + // what it prints and the toolpaths under it all agree on what is loaded there. static const std::array s_carriage_colors = {{ { 1.000f, 1.000f, 1.000f, 0.65f }, // primary — white { 0.941f, 0.894f, 0.259f, 0.65f }, // secondary 1 — yellow (#F0E442) @@ -2014,14 +2019,54 @@ void GCodeViewer::render_scene(int canvas_width, int canvas_height) m_imex_marker_key.mode_active_tools != active_tools) { m_imex_marker_key = ImexMarkerKey{ key, mode, mode_names, active_tools }; m_imex_marker_plan = resolve_imex_marker_plan(printer_cfg, mode, bed_extents); - // The carriage roster — and with it the marker count and colour order — - // can change with any of those inputs, so drop the markers and let the - // lazy init below rebuild them. + // The carriage roster — and with it the marker count — can change with any + // of those inputs, so drop the markers and let the lazy init below rebuild + // them. Their colours are restamped every frame regardless. m_sequential_view.m_imex_secondary_markers.clear(); } const ImexMarkerPlan& plan = m_imex_marker_plan; const int sec_count = (int)plan.carriages.size(); + + // Filament view colours the toolpaths by the filament printing them, so a + // carriage takes its own head's filament colour there and the palette + // everywhere else. get_imex_head_filament_color() answers UNPRINTABLE_COLOR + // for a head it cannot resolve, which is alpha 0.5 where a filament colour + // decodes opaque - that difference, not the black, is what distinguishes it, + // and a head with no filament keeps the palette rather than borrowing a + // colour that would read as a real assignment - the one point where a marker + // and the ghost of what it prints differ, the ghost having no palette to fall + // back to. + // + // The map and the pem are hoisted and each head resolved once, as + // PartPlate::update_imex_ghost_colors() does for the ghosts: both the marker + // and the toolhead box of a carriage want the same answer, and rebuilding it + // per draw parses the plate's map string and rebuilds the pem vector. + const bool color_by_filament = m_viewer.get_view_type() == libvgcode::EViewType::ColorPrint; + std::vector carriage_colors(sec_count + 1); + if (sec_count > 0) { + const ConfigOptionInts pem = effective_physical_extruder_map(*preset_bundle); + const auto plate_map = curr_plate ? curr_plate->get_imex_head_filament_map() + : std::map{}; + std::map head_colors; + auto color_for = [&](int phys_head, size_t palette_index) { + const ColorRGBA fallback = s_carriage_colors[palette_index % s_carriage_colors.size()]; + if (!color_by_filament || curr_plate == nullptr || phys_head < 0) + return fallback; + auto hc = head_colors.find(phys_head); + if (hc == head_colors.end()) + hc = head_colors.emplace(phys_head, + curr_plate->get_imex_head_filament_color(phys_head, pem, plate_map)).first; + if (hc->second == GLVolume::UNPRINTABLE_COLOR) + return fallback; + ColorRGBA c = hc->second; + c.a(fallback.a()); // as translucent as the palette makes them + return c; + }; + carriage_colors[0] = color_for(plan.pri_head, 0); + for (int i = 0; i < sec_count; ++i) + carriage_colors[i + 1] = color_for(plan.carriages[i].phys_head, i + 1); + } if (sec_count > 0) { imex_active = true; imex_box_wx = plan.box_wx; @@ -2032,13 +2077,19 @@ void GCodeViewer::render_scene(int canvas_width, int canvas_height) m_sequential_view.m_imex_secondary_markers.resize(sec_count); for (int i = 0; i < sec_count; ++i) { m_sequential_view.m_imex_secondary_markers[i].init(m_marker_filename); - m_sequential_view.m_imex_secondary_markers[i].set_color( - s_carriage_colors[(i + 1) % s_carriage_colors.size()]); } } + // Set every frame, not once at init: the view type and the filament palette + // both change without the marker list being rebuilt. The primary marker is + // one of the carriages here, so it is coloured alongside them - and restored + // below when no mode is active, since it is the same marker every non-IMEX + // preview draws. + m_sequential_view.marker.set_color(carriage_colors[0]); + for (int i = 0; i < sec_count; ++i) + m_sequential_view.m_imex_secondary_markers[i].set_color(carriage_colors[i + 1]); const Vec3f prim_pos = libvgcode::convert(curr_vertex.position); - carriage_box_draws.push_back({ prim_pos, s_carriage_colors[0], + carriage_box_draws.push_back({ prim_pos, carriage_colors[0], plan.pri_box_offset_x, plan.pri_box_offset_y }); for (int i = 0; i < sec_count; ++i) { @@ -2056,14 +2107,18 @@ void GCodeViewer::render_scene(int canvas_width, int canvas_height) m_sequential_view.m_imex_secondary_markers[i].set_world_position(sec_pos); m_sequential_view.m_imex_secondary_markers[i].set_z_offset(m_z_offset + 0.5f); carriage_box_draws.push_back({ - sec_pos, s_carriage_colors[(i + 1) % s_carriage_colors.size()], + sec_pos, carriage_colors[i + 1], carriage.box_offset_x, carriage.box_offset_y }); } } } } - if (!imex_active) + if (!imex_active) { m_sequential_view.m_imex_secondary_markers.clear(); + // The primary marker is the one every preview draws, so the carriage colour it may + // have been given has to come back off when no parallel mode is active. + m_sequential_view.marker.set_color(SequentialView::Marker::default_color()); + } } m_sequential_view.render_marker(!m_no_render_path, canvas_width, sequential_view_height(canvas_height), m_viewer.get_view_type()); diff --git a/src/slic3r/GUI/GCodeViewer.hpp b/src/slic3r/GUI/GCodeViewer.hpp index 9b496f0f2c..4f887cce76 100644 --- a/src/slic3r/GUI/GCodeViewer.hpp +++ b/src/slic3r/GUI/GCodeViewer.hpp @@ -103,6 +103,8 @@ public: bool is_visible() const { return m_visible; } void set_visible(bool visible) { m_visible = visible; } + // What a marker looks like when no IDEX/IQEX carriage colour applies to it. + static ColorRGBA default_color() { return { 1.0f, 1.0f, 1.0f, 0.5f }; } void set_color(const ColorRGBA& color) { m_model.set_color(color); } void render(int canvas_width, int canvas_height, const libvgcode::EViewType& view_type); @@ -228,6 +230,7 @@ private: // the zone geometry, so both are resolved up front. struct Carriage { + int phys_head = -1; // physical head this carriage is, for its filament colour bool mirror_x = false; bool mirror_y = false; float x_term = 0.0f; @@ -236,6 +239,7 @@ private: float box_offset_y = 0.0f; }; std::vector carriages; // empty => no secondary carriages to draw + int pri_head = -1; // the primary's physical head, same purpose float pri_box_offset_x = 0.0f; float pri_box_offset_y = 0.0f; float box_wx = 0.0f; // imex_nozzle_clearance_x / _y