From d50642cfd664f3170b249c1692e1c695e679dc71 Mon Sep 17 00:00:00 2001 From: PILIPALA030 <68017138+PILIPALA030@users.noreply.github.com> Date: Wed, 17 Jun 2026 20:45:47 +0800 Subject: [PATCH 1/2] Feature filament color adaptation This PR implements the official filament color management system for Snapmaker U1 printers, enabling users to select filament colors from an official color library with multi-color (dual-segment/gradient) visual support. Add Snapmaker official filament color management with multi-color metadata, official color selection dialog, and persisted self-described color data. Add filament color data structures, mode normalization, and hex/multi-color helpers Load official filament color catalog and match by material name with filament ID fallback Support official color selection, localized color names, SKU display, and custom color fallback Render single, segmented, and gradient filament colors in preset lists and paint toolbars Sync multi-color fields through preset/project configuration and preprint data Avoid writing Snapmaker-only SKU data into 3MF project settings Preserve existing mixed-filament badge behavior while adding official color bitmap rendering --- .../i18n/zh_CN/Snapmaker_Orca_zh_CN.po | 10 + resources/images/filament_color_more.svg | 4 + .../Snapmaker/filament/filaments_colours.json | 1802 +++++++++++++++++ src/libslic3r/AppConfig.hpp | 23 +- src/libslic3r/CMakeLists.txt | 2 + src/libslic3r/FilamentColorLibrary.cpp | 538 +++++ src/libslic3r/FilamentColorLibrary.hpp | 82 + src/libslic3r/PresetBundle.cpp | 194 +- src/libslic3r/PresetBundle.hpp | 4 + src/libslic3r/Print.cpp | 2 + src/libslic3r/PrintConfig.cpp | 17 + src/libslic3r/PrintConfig.hpp | 2 + src/slic3r/CMakeLists.txt | 4 + src/slic3r/GUI/FilamentColorDialog.cpp | 912 +++++++++ src/slic3r/GUI/FilamentColorDialog.hpp | 69 + src/slic3r/GUI/FilamentColorUtils.cpp | 169 ++ src/slic3r/GUI/FilamentColorUtils.hpp | 50 + src/slic3r/GUI/MixedFilamentBadge.cpp | 136 ++ src/slic3r/GUI/MixedFilamentBadge.hpp | 5 + src/slic3r/GUI/Plater.cpp | 2 + src/slic3r/GUI/PresetComboBoxes.cpp | 449 +++- src/slic3r/GUI/PresetComboBoxes.hpp | 8 +- src/slic3r/GUI/SSWCP.cpp | 70 +- src/slic3r/GUI/Tab.cpp | 51 +- src/slic3r/GUI/wxExtensions.cpp | 20 + 25 files changed, 4480 insertions(+), 145 deletions(-) create mode 100644 resources/images/filament_color_more.svg create mode 100644 resources/profiles/Snapmaker/filament/filaments_colours.json create mode 100644 src/libslic3r/FilamentColorLibrary.cpp create mode 100644 src/libslic3r/FilamentColorLibrary.hpp create mode 100644 src/slic3r/GUI/FilamentColorDialog.cpp create mode 100644 src/slic3r/GUI/FilamentColorDialog.hpp create mode 100644 src/slic3r/GUI/FilamentColorUtils.cpp create mode 100644 src/slic3r/GUI/FilamentColorUtils.hpp diff --git a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po index 030a2efc5a2..8d890acda23 100644 --- a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po +++ b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po @@ -15237,3 +15237,13 @@ msgstr "擦拭塔熨烫面积" msgid "Ironing area for prime tower interface layer (where different materials meet)." msgstr "擦拭塔的起始点熨烫面积" +msgstr "耗材%d参与混色的比例过高,可能会影响混色效果。" + +msgid "Official Filaments" +msgstr "官方耗材" + +msgid "Other Colors" +msgstr "其他颜色" + +msgid "Multiple Color" +msgstr "多色" \ No newline at end of file diff --git a/resources/images/filament_color_more.svg b/resources/images/filament_color_more.svg new file mode 100644 index 00000000000..33942357685 --- /dev/null +++ b/resources/images/filament_color_more.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/profiles/Snapmaker/filament/filaments_colours.json b/resources/profiles/Snapmaker/filament/filaments_colours.json new file mode 100644 index 00000000000..e56cc728d7e --- /dev/null +++ b/resources/profiles/Snapmaker/filament/filaments_colours.json @@ -0,0 +1,1802 @@ +{ + "vendor": "Snapmaker", + "updated_date": "2026-06-11", + "filaments": [ + { + "filament_id": "1682237920", + "filament_name": "Snapmaker ABS @U1", + "filament_type": "ABS", + "enabled": true, + "filament_color": [ + { + "sku": "34237", + "mode": 0, + "filament_color": [ + "#FFFFFF" + ], + "color_name": { + "en": "White", + "zh_CN": "白色" + }, + "enabled": true + }, + { + "sku": "34238", + "mode": 0, + "filament_color": [ + "#000000" + ], + "color_name": { + "en": "Black", + "zh_CN": "黑色" + }, + "enabled": true + }, + { + "sku": "34239", + "mode": 0, + "filament_color": [ + "#FFB81C" + ], + "color_name": { + "en": "Yellow", + "zh_CN": "黄色" + }, + "enabled": true + }, + { + "sku": "34240", + "mode": 0, + "filament_color": [ + "#DE4343" + ], + "color_name": { + "en": "Red", + "zh_CN": "红色" + }, + "enabled": true + }, + { + "sku": "34241", + "mode": 0, + "filament_color": [ + "#3A913F" + ], + "color_name": { + "en": "Green", + "zh_CN": "绿色" + }, + "enabled": true + }, + { + "sku": "34242", + "mode": 0, + "filament_color": [ + "#FF6A13" + ], + "color_name": { + "en": "Orange", + "zh_CN": "橙色" + }, + "enabled": true + }, + { + "sku": "34243", + "mode": 0, + "filament_color": [ + "#6E3FA3" + ], + "color_name": { + "en": "Purple", + "zh_CN": "紫色" + }, + "enabled": true + }, + { + "sku": "34244", + "mode": 0, + "filament_color": [ + "#5BC2E7" + ], + "color_name": { + "en": "Light Blue", + "zh_CN": "浅蓝色" + }, + "enabled": true + }, + { + "sku": "34245", + "mode": 0, + "filament_color": [ + "#002677" + ], + "color_name": { + "en": "Navy Blue", + "zh_CN": "海军蓝" + }, + "enabled": true + }, + { + "sku": "34246", + "mode": 0, + "filament_color": [ + "#54585A" + ], + "color_name": { + "en": "Dark Gray", + "zh_CN": "深灰色" + }, + "enabled": true + } + ] + }, + { + "filament_id": "12471727060", + "filament_name": "Snapmaker ASA @U1", + "filament_type": "ASA", + "enabled": true, + "filament_color": [ + { + "sku": "34252", + "mode": 0, + "filament_color": [ + "#FFE800" + ], + "color_name": { + "en": "Yellow", + "zh_CN": "黄色" + }, + "enabled": true + }, + { + "sku": "34253", + "mode": 0, + "filament_color": [ + "#AF231C" + ], + "color_name": { + "en": "Red", + "zh_CN": "红色" + }, + "enabled": true + }, + { + "sku": "34254", + "mode": 0, + "filament_color": [ + "#53565A" + ], + "color_name": { + "en": "Dark Gray", + "zh_CN": "深灰色" + }, + "enabled": true + }, + { + "sku": "34255", + "mode": 0, + "filament_color": [ + "#161618" + ], + "color_name": { + "en": "Black", + "zh_CN": "黑色" + }, + "enabled": true + }, + { + "sku": "34256", + "mode": 0, + "filament_color": [ + "#FFFFFF" + ], + "color_name": { + "en": "White", + "zh_CN": "白色" + }, + "enabled": true + } + ] + }, + { + "filament_id": "GFG96", + "filament_name": "Snapmaker PETG HF @U1", + "filament_type": "PETG HF", + "enabled": true, + "filament_color": [ + { + "sku": "34157", + "mode": 0, + "filament_color": [ + "#16171B" + ], + "color_name": { + "en": "Black", + "zh_CN": "黑色" + }, + "enabled": true + }, + { + "sku": "34158", + "mode": 0, + "filament_color": [ + "#F4F9FF" + ], + "color_name": { + "en": "White", + "zh_CN": "白色" + }, + "enabled": true + }, + { + "sku": "34159", + "mode": 0, + "filament_color": [ + "#ACB8C5" + ], + "color_name": { + "en": "Light Gray", + "zh_CN": "浅灰色" + }, + "enabled": true + }, + { + "sku": "34160", + "mode": 0, + "filament_color": [ + "#FF1523" + ], + "color_name": { + "en": "Red", + "zh_CN": "红色" + }, + "enabled": true + }, + { + "sku": "34161", + "mode": 0, + "filament_color": [ + "#FB7E1B" + ], + "color_name": { + "en": "Orange", + "zh_CN": "橙色" + }, + "enabled": true + }, + { + "sku": "34162", + "mode": 0, + "filament_color": [ + "#0049B8" + ], + "color_name": { + "en": "Cobalt Blue", + "zh_CN": "钴蓝色" + }, + "enabled": true + }, + { + "sku": "34163", + "mode": 0, + "filament_color": [ + "#7C818E" + ], + "color_name": { + "en": "Silver Gray", + "zh_CN": "银灰色" + }, + "enabled": true + }, + { + "sku": "34164", + "mode": 0, + "filament_color": [ + "#493AB2" + ], + "color_name": { + "en": "Violet", + "zh_CN": "紫罗兰" + }, + "enabled": true + }, + { + "sku": "34165", + "mode": 0, + "filament_color": [ + "#473378" + ], + "color_name": { + "en": "Deep Purple", + "zh_CN": "深紫色" + }, + "enabled": true + }, + { + "sku": "34166", + "mode": 0, + "filament_color": [ + "#6B504C" + ], + "color_name": { + "en": "Cocoa Brown", + "zh_CN": "可可棕" + }, + "enabled": true + }, + { + "sku": "34167", + "mode": 0, + "filament_color": [ + "#FBE95C" + ], + "color_name": { + "en": "Yellow", + "zh_CN": "黄色" + }, + "enabled": true + }, + { + "sku": "34168", + "mode": 0, + "filament_color": [ + "#38B837" + ], + "color_name": { + "en": "Green", + "zh_CN": "绿色" + }, + "enabled": true + }, + { + "sku": "34169", + "mode": 0, + "filament_color": [ + "#223622" + ], + "color_name": { + "en": "Dark Green", + "zh_CN": "深绿色" + }, + "enabled": true + }, + { + "sku": "34170", + "mode": 0, + "filament_color": [ + "#F5BEE9" + ], + "color_name": { + "en": "Light Pink", + "zh_CN": "浅粉色" + }, + "enabled": true + } + ] + }, + { + "filament_id": "PETG_TRANSLUCENT_001", + "filament_name": "Snapmaker PETG Translucent @U1", + "filament_type": "PETG Translucent", + "enabled": true, + "filament_color": [ + { + "sku": "34217", + "mode": 0, + "filament_color": [ + "#3DD1CE" + ], + "color_name": { + "en": "Translucent Teal", + "zh_CN": "透光蓝绿" + }, + "enabled": true + }, + { + "sku": "34218", + "mode": 0, + "filament_color": [ + "#338CC4" + ], + "color_name": { + "en": "Translucent Blue", + "zh_CN": "透光蓝色" + }, + "enabled": true + }, + { + "sku": "34219", + "mode": 0, + "filament_color": [ + "#7F8896" + ], + "color_name": { + "en": "Translucent Gray", + "zh_CN": "透光灰色" + }, + "enabled": true + }, + { + "sku": "34220", + "mode": 0, + "filament_color": [ + "#537A37" + ], + "color_name": { + "en": "Translucent Green", + "zh_CN": "透光绿色" + }, + "enabled": true + }, + { + "sku": "34221", + "mode": 0, + "filament_color": [ + "#92713F" + ], + "color_name": { + "en": "Translucent Brown", + "zh_CN": "透光棕色" + }, + "enabled": true + }, + { + "sku": "34222", + "mode": 0, + "filament_color": [ + "#FB8D02" + ], + "color_name": { + "en": "Translucent Orange", + "zh_CN": "透光橙色" + }, + "enabled": true + }, + { + "sku": "34223", + "mode": 0, + "filament_color": [ + "#E68FBD" + ], + "color_name": { + "en": "Translucent Pink", + "zh_CN": "透光粉色" + }, + "enabled": true + }, + { + "sku": "34224", + "mode": 0, + "filament_color": [ + "#D480EF" + ], + "color_name": { + "en": "Translucent Purple", + "zh_CN": "透光紫色" + }, + "enabled": true + } + ] + }, + { + "filament_id": "10425112260", + "filament_name": "Snapmaker PETG-CF @U1", + "filament_type": "PETG-CF", + "enabled": true, + "filament_color": [ + { + "sku": "34302", + "mode": 0, + "filament_color": [ + "#0D0D0D" + ], + "color_name": { + "en": "Black", + "zh_CN": "黑色" + }, + "enabled": true + }, + { + "sku": "34303", + "mode": 0, + "filament_color": [ + "#706399" + ], + "color_name": { + "en": "Purple", + "zh_CN": "紫色" + }, + "enabled": true + }, + { + "sku": "34304", + "mode": 0, + "filament_color": [ + "#C7653E" + ], + "color_name": { + "en": "Burnt Orange", + "zh_CN": "焦橙色" + }, + "enabled": true + }, + { + "sku": "34305", + "mode": 0, + "filament_color": [ + "#853E3E" + ], + "color_name": { + "en": "Dark Red", + "zh_CN": "深红色" + }, + "enabled": true + }, + { + "sku": "34306", + "mode": 0, + "filament_color": [ + "#237D7C" + ], + "color_name": { + "en": "Teal", + "zh_CN": "蓝绿色" + }, + "enabled": true + }, + { + "sku": "34307", + "mode": 0, + "filament_color": [ + "#2F4E93" + ], + "color_name": { + "en": "Deep Blue", + "zh_CN": "深蓝色" + }, + "enabled": true + }, + { + "sku": "34308", + "mode": 0, + "filament_color": [ + "#606068" + ], + "color_name": { + "en": "Dark Gray", + "zh_CN": "深灰色" + }, + "enabled": true + } + ] + }, + { + "filament_id": "14170311270", + "filament_name": "Snapmaker PLA Full Spectrum @U1", + "filament_type": "PLA Full Spectrum", + "enabled": true, + "filament_color": [ + { + "sku": "34262", + "mode": 0, + "filament_color": [ + "#08ABFB" + ], + "color_name": { + "en": "Semi-Translucent Cyan", + "zh_CN": "半透青色" + }, + "enabled": true + }, + { + "sku": "34263", + "mode": 0, + "filament_color": [ + "#D93B90" + ], + "color_name": { + "en": "Semi-Translucent Magenta", + "zh_CN": "半透品红" + }, + "enabled": true + }, + { + "sku": "34264", + "mode": 0, + "filament_color": [ + "#F9ED3D" + ], + "color_name": { + "en": "Semi-Translucent Yellow", + "zh_CN": "半透黄色" + }, + "enabled": true + }, + { + "sku": "34265", + "mode": 0, + "filament_color": [ + "#9199A4" + ], + "color_name": { + "en": "Semi-Translucent Gray", + "zh_CN": "半透灰色" + }, + "enabled": true + } + ] + }, + { + "filament_id": "141703112701", + "filament_name": "Snapmaker PLA Matte @U1", + "filament_type": "PLA Matte", + "enabled": true, + "filament_color": [ + { + "sku": "34076", + "mode": 0, + "filament_color": [ + "#FFFFFF" + ], + "color_name": { + "en": "Ivory White", + "zh_CN": "骨白色" + }, + "enabled": true + }, + { + "sku": "34077", + "mode": 0, + "filament_color": [ + "#F78E0E" + ], + "color_name": { + "en": "Lava Orange", + "zh_CN": "熔岩橙" + }, + "enabled": true + }, + { + "sku": "34078", + "mode": 0, + "filament_color": [ + "#F0BE02" + ], + "color_name": { + "en": "Sunburst Yellow", + "zh_CN": "日曜黄" + }, + "enabled": true + }, + { + "sku": "34079", + "mode": 0, + "filament_color": [ + "#000000" + ], + "color_name": { + "en": "Carbon Black", + "zh_CN": "碳素黑" + }, + "enabled": true + }, + { + "sku": "34080", + "mode": 0, + "filament_color": [ + "#BEC9A5" + ], + "color_name": { + "en": "Moss Mist", + "zh_CN": "苔雾绿" + }, + "enabled": true + }, + { + "sku": "34081", + "mode": 0, + "filament_color": [ + "#519F61" + ], + "color_name": { + "en": "Pine Green", + "zh_CN": "松木绿" + }, + "enabled": true + }, + { + "sku": "34082", + "mode": 0, + "filament_color": [ + "#DE1619" + ], + "color_name": { + "en": "Engine Red", + "zh_CN": "引擎红" + }, + "enabled": true + }, + { + "sku": "34083", + "mode": 0, + "filament_color": [ + "#485155" + ], + "color_name": { + "en": "Ash Grey", + "zh_CN": "烬灰色" + }, + "enabled": true + }, + { + "sku": "34084", + "mode": 0, + "filament_color": [ + "#8BD5EE" + ], + "color_name": { + "en": "Celestial Blue", + "zh_CN": "天幕蓝" + }, + "enabled": true + }, + { + "sku": "34085", + "mode": 0, + "filament_color": [ + "#F3D6E5" + ], + "color_name": { + "en": "Sakura Pink", + "zh_CN": "樱花粉" + }, + "enabled": true + }, + { + "sku": "34086", + "mode": 0, + "filament_color": [ + "#FC6371" + ], + "color_name": { + "en": "Watermelon Red", + "zh_CN": "西瓜红" + }, + "enabled": true + }, + { + "sku": "34087", + "mode": 0, + "filament_color": [ + "#0078BF" + ], + "color_name": { + "en": "Marine Blue", + "zh_CN": "海蓝色" + }, + "enabled": true + }, + { + "sku": "34088", + "mode": 0, + "filament_color": [ + "#D3B7A7" + ], + "color_name": { + "en": "Latte Brown", + "zh_CN": "拿铁棕" + }, + "enabled": true + }, + { + "sku": "34089", + "mode": 0, + "filament_color": [ + "#FEE5A5" + ], + "color_name": { + "en": "Butter Cream", + "zh_CN": "奶油黄" + }, + "enabled": true + }, + { + "sku": "34090", + "mode": 0, + "filament_color": [ + "#6600CC" + ], + "color_name": { + "en": "Violet Purple", + "zh_CN": "紫罗兰" + }, + "enabled": true + }, + { + "sku": "34091", + "mode": 0, + "filament_color": [ + "#AE96D4" + ], + "color_name": { + "en": "Lavender Purple", + "zh_CN": "薰衣草紫" + }, + "enabled": true + }, + { + "sku": "34092", + "mode": 0, + "filament_color": [ + "#775C50" + ], + "color_name": { + "en": "Mocha Brown", + "zh_CN": "摩卡棕" + }, + "enabled": true + }, + { + "sku": "34093", + "mode": 0, + "filament_color": [ + "#D4E458" + ], + "color_name": { + "en": "Lime Green", + "zh_CN": "青柠绿" + }, + "enabled": true + }, + { + "sku": "34094", + "mode": 0, + "filament_color": [ + "#68724D" + ], + "color_name": { + "en": "Olive Green", + "zh_CN": "橄榄绿" + }, + "enabled": true + }, + { + "sku": "34095", + "mode": 0, + "filament_color": [ + "#2E4462" + ], + "color_name": { + "en": "Steel Blue", + "zh_CN": "钢蓝色" + }, + "enabled": true + }, + { + "sku": "34097", + "mode": 0, + "filament_color": [ + "#56B7E6" + ], + "color_name": { + "en": "Sky Blue", + "zh_CN": "天空蓝" + }, + "enabled": true + }, + { + "sku": "34098", + "mode": 0, + "filament_color": [ + "#D84B2E" + ], + "color_name": { + "en": "Terracotta Red", + "zh_CN": "陶土红" + }, + "enabled": true + }, + { + "sku": "34099", + "mode": 0, + "filament_color": [ + "#ADB4E6" + ], + "color_name": { + "en": "Periwinkle", + "zh_CN": "长春花紫" + }, + "enabled": true + } + ] + }, + { + "filament_id": "11813638720", + "filament_name": "Snapmaker PLA Silk @U1", + "filament_type": "PLA Silk", + "enabled": true, + "filament_color": [ + { + "sku": "34137", + "mode": 0, + "filament_color": [ + "#BCC2C8" + ], + "color_name": { + "en": "Silver", + "zh_CN": "银色" + }, + "enabled": true + }, + { + "sku": "34138", + "mode": 0, + "filament_color": [ + "#C1443F" + ], + "color_name": { + "en": "Garnet Red", + "zh_CN": "石榴红" + }, + "enabled": true + }, + { + "sku": "34139", + "mode": 0, + "filament_color": [ + "#EFC2CA" + ], + "color_name": { + "en": "Pearl Pink", + "zh_CN": "珠光粉" + }, + "enabled": true + }, + { + "sku": "34140", + "mode": 0, + "filament_color": [ + "#768EC7" + ], + "color_name": { + "en": "Sapphire Blue", + "zh_CN": "宝石蓝" + }, + "enabled": true + }, + { + "sku": "34141", + "mode": 0, + "filament_color": [ + "#F6CE1B" + ], + "color_name": { + "en": "Gold", + "zh_CN": "金色" + }, + "enabled": true + }, + { + "sku": "34142", + "mode": 0, + "filament_color": [ + "#AF6B4C" + ], + "color_name": { + "en": "Copper", + "zh_CN": "铜色" + }, + "enabled": true + }, + { + "sku": "34143", + "mode": 0, + "filament_color": [ + "#A8FF54" + ], + "color_name": { + "en": "Neon Green", + "zh_CN": "荧光绿" + }, + "enabled": true + }, + { + "sku": "34144", + "mode": 0, + "filament_color": [ + "#4CC1CB" + ], + "color_name": { + "en": "Aqua Blue", + "zh_CN": "水蓝" + }, + "enabled": true + }, + { + "sku": "34145", + "mode": 0, + "filament_color": [ + "#55B687" + ], + "color_name": { + "en": "Emerald Green", + "zh_CN": "翡翠绿" + }, + "enabled": true + }, + { + "sku": "34146", + "mode": 0, + "filament_color": [ + "#DFE4E4" + ], + "color_name": { + "en": "Pearl White", + "zh_CN": "珠光白" + }, + "enabled": true + }, + { + "sku": "34202", + "mode": 0, + "filament_color": [ + "#D9A63A", "#CC434F" + ], + "color_name": { + "en": "Sunset Ember", + "zh_CN": "金红双色" + }, + "enabled": true + }, + { + "sku": "34203", + "mode": 0, + "filament_color": [ + "#D9A63A", "#9675CD" + ], + "color_name": { + "en": "Aurora Gold", + "zh_CN": "金紫双色" + }, + "enabled": true + }, + { + "sku": "34204", + "mode": 0, + "filament_color": [ + "#D9A63A", "#C4C7D9" + ], + "color_name": { + "en": "Solar Alloy", + "zh_CN": "金银双色" + }, + "enabled": true + }, + { + "sku": "34205", + "mode": 0, + "filament_color": [ + "#ECED17", "#44ADE5" + ], + "color_name": { + "en": "Mint Lemonade", + "zh_CN": "黄蓝双色" + }, + "enabled": true + }, + { + "sku": "34206", + "mode": 0, + "filament_color": [ + "#44ADE5", "#18CCAF" + ], + "color_name": { + "en": "Sea Glass", + "zh_CN": "蓝绿双色" + }, + "enabled": true + }, + { + "sku": "34207", + "mode": 0, + "filament_color": [ + "#C4C7D9", "#44ADE5" + ], + "color_name": { + "en": "Ice Lake", + "zh_CN": "银蓝双色" + }, + "enabled": true + }, + { + "sku": "34208", + "mode": 0, + "filament_color": [ + "#CBF914", "#D623AA" + ], + "color_name": { + "en": "City Billboard", + "zh_CN": "黄红双色" + }, + "enabled": true + } + ] + }, + { + "filament_id": "141703112701", + "filament_name": "Snapmaker PLA SnapSpeed @U1", + "filament_type": "PLA SnapSpeed", + "enabled": true, + "filament_color": [ + { + "sku": "34061", + "mode": 0, + "filament_color": [ + "#E2DEDB" + ], + "color_name": { + "en": "Pearl White", + "zh_CN": "珍珠白" + }, + "enabled": true + }, + { + "sku": "34062", + "mode": 0, + "filament_color": [ + "#080A0D" + ], + "color_name": { + "en": "Black", + "zh_CN": "黑色" + }, + "enabled": true + }, + { + "sku": "34063", + "mode": 0, + "filament_color": [ + "#8C9099" + ], + "color_name": { + "en": "Grey", + "zh_CN": "灰色" + }, + "enabled": true + }, + { + "sku": "34064", + "mode": 0, + "filament_color": [ + "#003776" + ], + "color_name": { + "en": "Blue", + "zh_CN": "蓝色" + }, + "enabled": true + }, + { + "sku": "34065", + "mode": 0, + "filament_color": [ + "#E72F1D" + ], + "color_name": { + "en": "Red", + "zh_CN": "红色" + }, + "enabled": true + }, + { + "sku": "34066", + "mode": 0, + "filament_color": [ + "#6C5BB1" + ], + "color_name": { + "en": "Purple", + "zh_CN": "紫色" + }, + "enabled": true + }, + { + "sku": "34067", + "mode": 0, + "filament_color": [ + "#F97429" + ], + "color_name": { + "en": "Orange", + "zh_CN": "橙色" + }, + "enabled": true + }, + { + "sku": "34068", + "mode": 0, + "filament_color": [ + "#2D9E59" + ], + "color_name": { + "en": "Green", + "zh_CN": "绿色" + }, + "enabled": true + }, + { + "sku": "34069", + "mode": 0, + "filament_color": [ + "#F4C032" + ], + "color_name": { + "en": "Yellow", + "zh_CN": "黄色" + }, + "enabled": true + }, + { + "sku": "34070", + "mode": 0, + "filament_color": [ + "#6F4C2F" + ], + "color_name": { + "en": "Brown", + "zh_CN": "棕色" + }, + "enabled": true + }, + { + "sku": "34071", + "mode": 0, + "filament_color": [ + "#FFC0CB" + ], + "color_name": { + "en": "Pink", + "zh_CN": "粉色" + }, + "enabled": true + }, + { + "sku": "34072", + "mode": 0, + "filament_color": [ + "#FFF5E0" + ], + "color_name": { + "en": "Cream", + "zh_CN": "奶油白" + }, + "enabled": true + }, + { + "sku": "34073", + "mode": 0, + "filament_color": [ + "#D9DFE5" + ], + "color_name": { + "en": "Cool White", + "zh_CN": "冷白色" + }, + "enabled": true + }, + { + "sku": "34074", + "mode": 0, + "filament_color": [ + "#F24574" + ], + "color_name": { + "en": "Magenta", + "zh_CN": "品红色" + }, + "enabled": true + }, + { + "sku": "34075", + "mode": 0, + "filament_color": [ + "#5EBDDB" + ], + "color_name": { + "en": "Sky Blue", + "zh_CN": "天空蓝" + }, + "enabled": true + }, + { + "sku": "34112", + "mode": 0, + "filament_color": [ + "#F8F81C" + ], + "color_name": { + "en": "Bright Yellow", + "zh_CN": "亮黄色" + }, + "enabled": true + }, + { + "sku": "34113", + "mode": 0, + "filament_color": [ + "#AFC33C" + ], + "color_name": { + "en": "Olive Green", + "zh_CN": "橄榄绿" + }, + "enabled": true + }, + { + "sku": "34114", + "mode": 0, + "filament_color": [ + "#94B7C9" + ], + "color_name": { + "en": "Blue Gray", + "zh_CN": "蓝灰色" + }, + "enabled": true + }, + { + "sku": "34115", + "mode": 0, + "filament_color": [ + "#C19E86" + ], + "color_name": { + "en": "Warm Beige", + "zh_CN": "暖米色" + }, + "enabled": true + }, + { + "sku": "34116", + "mode": 0, + "filament_color": [ + "#674B47" + ], + "color_name": { + "en": "Cocoa Brown", + "zh_CN": "可可棕" + }, + "enabled": true + }, + { + "sku": "34117", + "mode": 0, + "filament_color": [ + "#0086D6" + ], + "color_name": { + "en": "Cyan", + "zh_CN": "青色" + }, + "enabled": true + }, + { + "sku": "34118", + "mode": 0, + "filament_color": [ + "#EC008C" + ], + "color_name": { + "en": "Vivid Magenta", + "zh_CN": "亮品红色" + }, + "enabled": true + }, + { + "sku": "34119", + "mode": 0, + "filament_color": [ + "#FCE300" + ], + "color_name": { + "en": "Lemon Yellow", + "zh_CN": "柠檬黄色" + }, + "enabled": true + }, + { + "sku": "34120", + "mode": 0, + "filament_color": [ + "#FFFFFF" + ], + "color_name": { + "en": "Pure White", + "zh_CN": "纯白色" + }, + "enabled": true + } + ] + }, + { + "filament_id": "SPTR001", + "filament_name": "Snapmaker PLA Translucent @U1", + "filament_type": "PLA Translucent", + "enabled": true, + "filament_color": [ + { + "sku": "34322", + "mode": 0, + "filament_color": [ + "#C08559" + ], + "color_name": { + "en": "Translucent Amber", + "zh_CN": "透光琥珀色" + }, + "enabled": true + }, + { + "sku": "34323", + "mode": 0, + "filament_color": [ + "#FFF200" + ], + "color_name": { + "en": "Translucent Yellow", + "zh_CN": "透光黄色" + }, + "enabled": true + }, + { + "sku": "34324", + "mode": 0, + "filament_color": [ + "#008729" + ], + "color_name": { + "en": "Translucent Green", + "zh_CN": "透光绿色" + }, + "enabled": true + }, + { + "sku": "34325", + "mode": 0, + "filament_color": [ + "#7FEBE2" + ], + "color_name": { + "en": "Translucent Cyan", + "zh_CN": "透光青色" + }, + "enabled": true + }, + { + "sku": "34326", + "mode": 0, + "filament_color": [ + "#B1ACA6" + ], + "color_name": { + "en": "Translucent Light Gray", + "zh_CN": "透光浅灰色" + }, + "enabled": true + }, + { + "sku": "34327", + "mode": 0, + "filament_color": [ + "#A7A8D9" + ], + "color_name": { + "en": "Translucent Light Purple", + "zh_CN": "透光浅紫色" + }, + "enabled": true + }, + { + "sku": "34328", + "mode": 0, + "filament_color": [ + "#46BAE0" + ], + "color_name": { + "en": "Translucent Blue", + "zh_CN": "透光蓝色" + }, + "enabled": true + }, + { + "sku": "34329", + "mode": 0, + "filament_color": [ + "#EEBCC8" + ], + "color_name": { + "en": "Translucent Pink", + "zh_CN": "透光粉色" + }, + "enabled": true + }, + { + "sku": "34330", + "mode": 0, + "filament_color": [ + "#DC0080" + ], + "color_name": { + "en": "Translucent Magenta", + "zh_CN": "透光品红" + }, + "enabled": true + }, + { + "sku": "34331", + "mode": 0, + "filament_color": [ + "#990002" + ], + "color_name": { + "en": "Translucent Dark Red", + "zh_CN": "透光深红色" + }, + "enabled": true + } + ] + }, + { + "filament_id": "17021473250", + "filament_name": "Snapmaker PLA-CF @U1", + "filament_type": "PLA-CF", + "enabled": true, + "filament_color": [ + { + "sku": "34227", + "mode": 0, + "filament_color": [ + "#823F2E" + ], + "color_name": { + "en": "Wine Red", + "zh_CN": "酒红色" + }, + "enabled": true + }, + { + "sku": "34228", + "mode": 0, + "filament_color": [ + "#6F8653" + ], + "color_name": { + "en": "Matcha Green", + "zh_CN": "抹茶绿" + }, + "enabled": true + }, + { + "sku": "34229", + "mode": 0, + "filament_color": [ + "#54555B" + ], + "color_name": { + "en": "Graphite Gray", + "zh_CN": "石墨灰" + }, + "enabled": true + }, + { + "sku": "34230", + "mode": 0, + "filament_color": [ + "#725E7C" + ], + "color_name": { + "en": "Violet Purple", + "zh_CN": "紫罗兰色" + }, + "enabled": true + }, + { + "sku": "34231", + "mode": 0, + "filament_color": [ + "#9B7C56" + ], + "color_name": { + "en": "Chestnut Brown", + "zh_CN": "栗棕色" + }, + "enabled": true + }, + { + "sku": "34232", + "mode": 0, + "filament_color": [ + "#49799D" + ], + "color_name": { + "en": "Denim Blue", + "zh_CN": "丹宁蓝" + }, + "enabled": true + }, + { + "sku": "34233", + "mode": 0, + "filament_color": [ + "#000000" + ], + "color_name": { + "en": "Black", + "zh_CN": "黑色" + }, + "enabled": true + } + ] + }, + { + "filament_id": "GFU99", + "filament_name": "Snapmaker TPU 90A @U1", + "filament_type": "TPU 90A", + "enabled": true, + "filament_color": [ + { + "sku": "34190", + "mode": 0, + "filament_color": [ + "#16161A" + ], + "color_name": { + "en": "Black", + "zh_CN": "黑色" + }, + "enabled": true + }, + { + "sku": "34191", + "mode": 0, + "filament_color": [ + "#B2B4B2" + ], + "color_name": { + "en": "Grey", + "zh_CN": "灰色" + }, + "enabled": true + }, + { + "sku": "34192", + "mode": 0, + "filament_color": [ + "#F8F8F5" + ], + "color_name": { + "en": "White", + "zh_CN": "白色" + }, + "enabled": true + } + ] + }, + { + "filament_id": "GFU99", + "filament_name": "Snapmaker TPU 95A HF @U1", + "filament_type": "TPU 95A HF", + "enabled": true, + "filament_color": [ + { + "sku": "34176", + "mode": 0, + "filament_color": [ + "#000000" + ], + "color_name": { + "en": "Black", + "zh_CN": "黑色" + }, + "enabled": true + }, + { + "sku": "34177", + "mode": 0, + "filament_color": [ + "#FFFFFF" + ], + "color_name": { + "en": "White", + "zh_CN": "白色" + }, + "enabled": true + }, + { + "sku": "34178", + "mode": 0, + "filament_color": [ + "#9E9E9E" + ], + "color_name": { + "en": "Silver Gray", + "zh_CN": "银灰色" + }, + "enabled": true + }, + { + "sku": "34179", + "mode": 0, + "filament_color": [ + "#865B41" + ], + "color_name": { + "en": "Brown", + "zh_CN": "棕色" + }, + "enabled": true + }, + { + "sku": "34180", + "mode": 0, + "filament_color": [ + "#0683EF" + ], + "color_name": { + "en": "Bright Blue", + "zh_CN": "亮蓝色" + }, + "enabled": true + }, + { + "sku": "34181", + "mode": 0, + "filament_color": [ + "#EE2A20" + ], + "color_name": { + "en": "Red", + "zh_CN": "红色" + }, + "enabled": true + }, + { + "sku": "34182", + "mode": 0, + "filament_color": [ + "#595E69" + ], + "color_name": { + "en": "Dark Gray", + "zh_CN": "深灰色" + }, + "enabled": true + }, + { + "sku": "34183", + "mode": 0, + "filament_color": [ + "#FFC72C" + ], + "color_name": { + "en": "Yellow", + "zh_CN": "黄色" + }, + "enabled": true + }, + { + "sku": "34184", + "mode": 0, + "filament_color": [ + "#33A15A" + ], + "color_name": { + "en": "Green", + "zh_CN": "绿色" + }, + "enabled": true + } + ] + } + ] +} \ No newline at end of file diff --git a/src/libslic3r/AppConfig.hpp b/src/libslic3r/AppConfig.hpp index a1251d1ff99..f8bbc8d34f1 100644 --- a/src/libslic3r/AppConfig.hpp +++ b/src/libslic3r/AppConfig.hpp @@ -234,19 +234,24 @@ class AppConfig m_printer_settings.erase(printer); m_dirty = true; } - bool has_printer_setting(std::string printer, std::string name) { - if (!has_printer_settings(printer)) + bool has_printer_setting(const std::string& printer, const std::string& name) const { + MachineSettingMap::const_iterator printerIt = m_printer_settings.find(printer); + if (printerIt == m_printer_settings.end()) return false; - if (!m_printer_settings[printer].contains(name)) - return false; - return true; + return printerIt->second.contains(name); } - std::string get_printer_setting(std::string printer, std::string name) { - if (!has_printer_setting(printer, name)) + std::string get_printer_setting(const std::string& printer, const std::string& name) const { + MachineSettingMap::const_iterator printerIt = m_printer_settings.find(printer); + if (printerIt == m_printer_settings.end()) + return ""; + + nlohmann::json::const_iterator settingIt = printerIt->second.find(name); + if (settingIt == printerIt->second.end() || !settingIt.value().is_string()) return ""; - return m_printer_settings[printer][name]; + + return settingIt.value().get(); } - void set_printer_setting(std::string printer, std::string name, std::string value) { + void set_printer_setting(const std::string& printer, const std::string& name, const std::string& value) { m_printer_settings[printer][name] = value; m_dirty = true; } diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index d830c5e51e9..3288c3f55ff 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -132,6 +132,8 @@ set(lisbslic3r_sources Feature/Interlocking/VoxelUtils.cpp Feature/Interlocking/VoxelUtils.hpp FileParserError.hpp + FilamentColorLibrary.cpp + FilamentColorLibrary.hpp FilamentHotBedNozzleRules.cpp FilamentHotBedNozzleRules.hpp Fill/Fill3DHoneycomb.cpp diff --git a/src/libslic3r/FilamentColorLibrary.cpp b/src/libslic3r/FilamentColorLibrary.cpp new file mode 100644 index 00000000000..d338287a8fa --- /dev/null +++ b/src/libslic3r/FilamentColorLibrary.cpp @@ -0,0 +1,538 @@ +#include "FilamentColorLibrary.hpp" +#include "Utils.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace Slic3r +{ +namespace +{ + +std::string TrimCopy(const std::string& value) +{ + std::string::const_iterator begin = std::find_if_not(value.begin(), value.end(), + [](unsigned char ch) + { + return std::isspace(ch); + }); + std::string::const_iterator end = std::find_if_not(value.rbegin(), value.rend(), + [](unsigned char ch) + { + return std::isspace(ch); + }).base(); + return begin < end ? std::string(begin, end) : std::string(); +} + +bool EndsWithCaseInsensitive(const std::string& value, const std::string& suffix) +{ + if (value.empty() || suffix.empty() || value.size() < suffix.size()) + return false; + + const size_t offset = value.size() - suffix.size(); + for (size_t index = 0; index < suffix.size(); ++index) + { + const unsigned char left = static_cast(value[offset + index]); + const unsigned char right = static_cast(suffix[index]); + if (std::tolower(left) != std::tolower(right)) + return false; + } + + return true; +} + +std::string NormalizeHexColorNoFallback(const std::string& color) +{ + std::string value = TrimCopy(color); + if (!value.empty() && value.front() == '#') + value.erase(value.begin()); + if (value.size() == 8) + value = value.substr(0, 6); + if (value.size() != 6) + return {}; + + for (char& ch : value) + { + const unsigned char uch = static_cast(ch); + if (!std::isxdigit(uch)) + return {}; + ch = static_cast(std::toupper(uch)); + } + + return "#" + value; +} + +std::vector NormalizeColorList(const std::vector& colors) +{ + std::vector normalizedColors; + normalizedColors.reserve(colors.size()); + for (const std::string& color : colors) + { + const std::string normalized = NormalizeFilamentHexColor(color); + if (!normalized.empty()) + normalizedColors.emplace_back(normalized); + } + return normalizedColors; +} + +boost::filesystem::path FilamentsColoursPath() +{ + // Prefer the system copy, fall back to bundled resources for a fresh install. + boost::filesystem::path path = boost::filesystem::path(Slic3r::data_dir()); + path /= "system"; + path /= "Snapmaker"; + path /= "filament"; + path /= "filaments_colours.json"; + if (boost::filesystem::exists(path)) + return path.make_preferred(); + + path = boost::filesystem::path(Slic3r::resources_dir()); + path /= "profiles"; + path /= "Snapmaker"; + path /= "filament"; + path /= "filaments_colours.json"; + return path.make_preferred(); +} + +std::string JsonString(const nlohmann::json& j, const char* key) +{ + nlohmann::json::const_iterator it = j.find(key); + return it != j.end() && it->is_string() ? it->get() : std::string(); +} + +int JsonMode(const nlohmann::json& j) +{ + nlohmann::json::const_iterator it = j.find("mode"); + if (it == j.end() || !it->is_number_integer()) + return 0; + return it->get() == 1 ? 1 : 0; +} + +bool JsonBool(const nlohmann::json& j, const char* key, bool defaultValue) +{ + nlohmann::json::const_iterator it = j.find(key); + return it != j.end() && it->is_boolean() ? it->get() : defaultValue; +} + +std::unordered_map JsonStringMap(const nlohmann::json& j, const char* key) +{ + std::unordered_map values; + nlohmann::json::const_iterator objectIt = j.find(key); + if (objectIt == j.end() || !objectIt->is_object()) + return values; + + for (nlohmann::json::const_iterator it = objectIt->begin(); it != objectIt->end(); ++it) + { + if (it.value().is_string()) + values.emplace(it.key(), it.value().get()); + } + return values; +} + +std::vector JsonStringArray(const nlohmann::json& j, const char* key) +{ + std::vector values; + nlohmann::json::const_iterator it = j.find(key); + if (it == j.end() || !it->is_array()) + return values; + + for (const nlohmann::json& item : *it) + { + if (item.is_string()) + values.emplace_back(item.get()); + } + return values; +} + +std::string NormalizeJsonColor(const std::string& color, bool& hasInvalidColor) +{ + const std::string normalized = NormalizeFilamentHexColor(color); + if (!color.empty() && normalized.empty()) + hasInvalidColor = true; + return normalized; +} + +bool LoadJsonFile(const boost::filesystem::path& path, nlohmann::json& out) +{ + boost::nowide::ifstream ifs(path.string()); + if (!ifs.is_open()) + { + BOOST_LOG_TRIVIAL(warning) << "Failed to open official filament color file: " << path.string(); + return false; + } + + out = nlohmann::json::parse(ifs, nullptr, false); + if (out.is_discarded()) + { + BOOST_LOG_TRIVIAL(warning) << "Failed to parse official filament color file: " << path.string(); + return false; + } + + return true; +} + +} // namespace + +std::string NormalizeFilamentHexColor(const std::string& color) +{ + return NormalizeHexColorNoFallback(color); +} + +std::string NormalizeFilamentHexColor(const std::string& color, const std::string& fallbackColor) +{ + const std::string normalized = NormalizeHexColorNoFallback(color); + if (!normalized.empty()) + return normalized; + + if (color == fallbackColor) + return {}; + return NormalizeHexColorNoFallback(fallbackColor); +} + +std::vector SplitFilamentMultiColors(const std::string& value) +{ + std::vector colors; + std::stringstream stream(value); + std::string token; + + while (std::getline(stream, token, '|')) + { + const std::string normalized = NormalizeFilamentHexColor(token); + if (!normalized.empty()) + colors.emplace_back(normalized); + } + + return colors; +} + +std::string JoinFilamentMultiColors(const std::vector& colors) +{ + std::ostringstream out; + bool first = true; + for (const std::string& color : colors) + { + const std::string normalized = NormalizeFilamentHexColor(color); + if (normalized.empty()) + continue; + if (!first) + out << '|'; + out << normalized; + first = false; + } + return out.str(); +} + +std::string GetFilamentMatchName(const std::string& name) +{ + std::string matchName = TrimCopy(name); + const std::string nozzleSuffix = " nozzle"; + if (EndsWithCaseInsensitive(matchName, nozzleSuffix)) + { + matchName = TrimCopy(matchName.substr(0, matchName.size() - nozzleSuffix.size())); + const size_t nozzlePos = matchName.find_last_of(" \t\r\n"); + if (nozzlePos != std::string::npos) + matchName = matchName.substr(0, nozzlePos); + } + + return TrimCopy(matchName); +} + +FilamentColorMode FilamentColorModeFromConfig(int modeValue) +{ + if (modeValue == static_cast(FilamentColorMode::Gradient)) + return FilamentColorMode::Gradient; + return FilamentColorMode::Segment; +} + +int FilamentColorModeToConfig(FilamentColorMode mode) +{ + if (mode == FilamentColorMode::Gradient) + return static_cast(FilamentColorMode::Gradient); + return static_cast(FilamentColorMode::Segment); +} + +bool FilamentColor::Empty() const +{ + return colors.empty(); +} + +FilamentColorMode FilamentColor::NormalizedMode() const +{ + if (colors.size() > 1 && mode == FilamentColorMode::Gradient) + return FilamentColorMode::Gradient; + return FilamentColorMode::Segment; +} + +bool FilamentColor::IsGradient() const +{ + return NormalizedMode() == FilamentColorMode::Gradient; +} + +std::string FilamentColor::PrimaryColor(const std::string& fallbackColor) const +{ + for (const std::string& color : colors) + { + const std::string normalized = NormalizeFilamentHexColor(color); + if (!normalized.empty()) + return normalized; + } + return NormalizeFilamentHexColor(fallbackColor, "#26A69A"); +} + +std::string FilamentColor::ToMultiColorsString() const +{ + return JoinFilamentMultiColors(colors); +} + +bool FilamentColor::Matches(const FilamentColor& other) const +{ + FilamentColor left = FromColors(colors, mode); + FilamentColor right = FromColors(other.colors, other.mode); + return left.colors == right.colors && left.NormalizedMode() == right.NormalizedMode(); +} + +FilamentColor FilamentColor::FromColors(const std::vector& colors, FilamentColorMode mode, + const std::string& fallbackColor) +{ + FilamentColor filamentColor; + filamentColor.colors = NormalizeColorList(colors); + if (filamentColor.colors.empty()) + { + const std::string fallback = NormalizeFilamentHexColor(fallbackColor, "#26A69A"); + filamentColor.colors.emplace_back(fallback.empty() ? "#26A69A" : fallback); + } + + filamentColor.mode = filamentColor.colors.size() > 1 ? mode : FilamentColorMode::Segment; + return filamentColor; +} + +FilamentColor FilamentColor::FromMultiColors(const std::string& multiColors, FilamentColorMode mode, + const std::string& fallbackColor) +{ + return FromColors(SplitFilamentMultiColors(multiColors), mode, fallbackColor); +} + +FilamentColorLibrary& FilamentColorLibrary::Instance() +{ + static FilamentColorLibrary library; + return library; +} + +bool FilamentColorLibrary::EnsureLoaded() +{ + if (_loaded) + return true; + + Clear(); + if (!LoadIndex()) + { + Clear(); + return false; + } + _loaded = true; + return true; +} + +void FilamentColorLibrary::Reload() +{ + Clear(); + _loaded = false; + EnsureLoaded(); +} + +bool FilamentColorLibrary::FindFilamentById(const std::string& filamentId, FilamentColorInfo& outFilament) +{ + if (!EnsureLoaded()) + return false; + + std::unordered_map::const_iterator filamentIt = _filamentIndexById.find(filamentId); + if (filamentIt == _filamentIndexById.end()) + return false; + + return FindFilamentByIndex(filamentIt->second, outFilament); +} + +bool FilamentColorLibrary::FindFilamentByName(const std::string& filamentName, FilamentColorInfo& outFilament) +{ + if (!EnsureLoaded()) + return false; + + const std::string matchName = GetFilamentMatchName(filamentName); + if (matchName.empty()) + return false; + + std::unordered_map::const_iterator nameIt = _filamentIndexByName.find(matchName); + return nameIt != _filamentIndexByName.end() ? FindFilamentByIndex(nameIt->second, outFilament) : false; +} + +bool FilamentColorLibrary::FindFilamentByIndex(size_t index, FilamentColorInfo& outFilament) const +{ + if (index >= _filaments.size()) + return false; + + outFilament = _filaments[index]; + return true; +} + +bool FilamentColorLibrary::LoadIndex() +{ + nlohmann::json root; + const boost::filesystem::path filePath = FilamentsColoursPath(); + if (!LoadJsonFile(filePath, root)) + return false; + + nlohmann::json::const_iterator filamentsIt = root.find("filaments"); + if (filamentsIt == root.end() || !filamentsIt->is_array()) + { + BOOST_LOG_TRIVIAL(warning) << "Missing official filament color filaments array: " << filePath.string(); + return false; + } + + std::vector filaments; + std::unordered_map filamentIndexById; + std::unordered_map filamentIndexByName; + std::unordered_set skus; + + for (const nlohmann::json& filamentJson : *filamentsIt) + { + if (!filamentJson.is_object()) + { + BOOST_LOG_TRIVIAL(warning) << "Skip invalid official filament item: " << filePath.string(); + continue; + } + + if (!JsonBool(filamentJson, "enabled", true)) + continue; + + FilamentColorInfo filament; + filament.filamentId = JsonString(filamentJson, "filament_id"); + filament.filamentName = JsonString(filamentJson, "filament_name"); + filament.type = JsonString(filamentJson, "filament_type"); + + if (filament.filamentId.empty() || filament.filamentName.empty()) + { + BOOST_LOG_TRIVIAL(warning) << "Skip official filament without id or name: " << filePath.string(); + continue; + } + + const std::string matchName = GetFilamentMatchName(filament.filamentName); + if (matchName.empty()) + { + BOOST_LOG_TRIVIAL(warning) << "Skip official filament without valid name: " << filament.filamentName; + continue; + } + + if (filamentIndexByName.find(matchName) != filamentIndexByName.end()) + { + BOOST_LOG_TRIVIAL(warning) << "Skip duplicate official filament name: " << filament.filamentName; + continue; + } + + const bool duplicateFilamentId = filamentIndexById.find(filament.filamentId) != filamentIndexById.end(); + if (duplicateFilamentId) + BOOST_LOG_TRIVIAL(warning) << "Official filament id already exists, keep name fallback only: " + << filament.filamentId; + + nlohmann::json::const_iterator colorsIt = filamentJson.find("filament_color"); + if (colorsIt == filamentJson.end() || !colorsIt->is_array()) + { + BOOST_LOG_TRIVIAL(warning) << "Skip official filament without colors array: " << filament.filamentId; + continue; + } + + for (const nlohmann::json& colorJson : *colorsIt) + { + if (!colorJson.is_object()) + { + BOOST_LOG_TRIVIAL(warning) << "Skip invalid color item for official filament: " << filament.filamentId; + continue; + } + + if (!JsonBool(colorJson, "enabled", true)) + continue; + + FilamentColorItem colorItem; + colorItem.sku = JsonString(colorJson, "sku"); + colorItem.colorNames = JsonStringMap(colorJson, "color_name"); + colorItem.colorData.mode = FilamentColorModeFromConfig(JsonMode(colorJson)); + + bool hasInvalidColor = false; + for (const std::string& rawColor : JsonStringArray(colorJson, "filament_color")) + { + const std::string normalizedColor = NormalizeJsonColor(rawColor, hasInvalidColor); + if (!normalizedColor.empty()) + colorItem.colorData.colors.emplace_back(normalizedColor); + } + + if (hasInvalidColor) + { + BOOST_LOG_TRIVIAL(warning) << "Skip color item with invalid color value: " << colorItem.sku; + continue; + } + + if (colorItem.sku.empty() || colorItem.colorData.colors.empty()) + { + BOOST_LOG_TRIVIAL(warning) << "Skip incomplete color item for official filament: " + << filament.filamentId; + continue; + } + + if ((colorItem.colorData.mode == FilamentColorMode::Gradient && colorItem.colorData.colors.size() < 2) || + (colorItem.colorData.mode == FilamentColorMode::Segment && colorItem.colorData.colors.size() > 2)) + { + // Gradient requires at least 2 colors; segment supports at most 2 colors. + BOOST_LOG_TRIVIAL(warning) << "Skip color item with invalid color count: " << colorItem.sku; + continue; + } + + if (skus.find(colorItem.sku) != skus.end()) + { + BOOST_LOG_TRIVIAL(warning) << "Skip duplicate official filament color SKU: " << colorItem.sku; + continue; + } + + skus.emplace(colorItem.sku); + filament.colors.emplace_back(std::move(colorItem)); + } + + if (filament.colors.empty()) + { + BOOST_LOG_TRIVIAL(warning) << "Skip official filament without valid colors: " << filament.filamentId; + continue; + } + + const size_t filamentIndex = filaments.size(); + filamentIndexByName.emplace(matchName, filamentIndex); + if (!duplicateFilamentId) + filamentIndexById.emplace(filament.filamentId, filamentIndex); + filaments.emplace_back(std::move(filament)); + } + + if (filaments.empty()) + { + BOOST_LOG_TRIVIAL(warning) << "Official filament color file has no valid filaments: " << filePath.string(); + return false; + } + + _filaments = std::move(filaments); + _filamentIndexById = std::move(filamentIndexById); + _filamentIndexByName = std::move(filamentIndexByName); + return true; +} + +void FilamentColorLibrary::Clear() +{ + _filaments.clear(); + _filamentIndexById.clear(); + _filamentIndexByName.clear(); +} + +} // namespace Slic3r diff --git a/src/libslic3r/FilamentColorLibrary.hpp b/src/libslic3r/FilamentColorLibrary.hpp new file mode 100644 index 00000000000..276e2c1d97e --- /dev/null +++ b/src/libslic3r/FilamentColorLibrary.hpp @@ -0,0 +1,82 @@ +#pragma once + +#include +#include +#include +#include + +namespace Slic3r +{ + +enum class FilamentColorMode +{ + Segment = 0, //Single colors or side by side segments + Gradient = 1 +}; + +std::string NormalizeFilamentHexColor(const std::string& color); +std::string NormalizeFilamentHexColor(const std::string& color, const std::string& fallbackColor); +std::vector SplitFilamentMultiColors(const std::string& value); +std::string JoinFilamentMultiColors(const std::vector& colors); +std::string GetFilamentMatchName(const std::string& name); +FilamentColorMode FilamentColorModeFromConfig(int modeValue); +int FilamentColorModeToConfig(FilamentColorMode mode); + +struct FilamentColor +{ + std::vector colors; + FilamentColorMode mode { FilamentColorMode::Segment }; + + bool Empty() const; + FilamentColorMode NormalizedMode() const; + bool IsGradient() const; + std::string PrimaryColor(const std::string& fallbackColor = "#26A69A") const; + std::string ToMultiColorsString() const; + bool Matches(const FilamentColor& other) const; + + static FilamentColor FromColors(const std::vector& colors, FilamentColorMode mode, + const std::string& fallbackColor = "#26A69A"); + static FilamentColor FromMultiColors(const std::string& multiColors, FilamentColorMode mode, + const std::string& fallbackColor = "#26A69A"); +}; + +struct FilamentColorItem +{ + std::unordered_map colorNames; + std::string sku; + FilamentColor colorData; +}; + +struct FilamentColorInfo +{ + std::string filamentId; + std::string filamentName; + std::string type; + std::vector colors; +}; + +class FilamentColorLibrary +{ +public: + static FilamentColorLibrary& Instance(); + + bool EnsureLoaded(); + void Reload(); + + bool FindFilamentById(const std::string& filamentId, FilamentColorInfo& outFilament); + bool FindFilamentByName(const std::string& filamentName, FilamentColorInfo& outFilament); + +private: + bool LoadIndex(); + bool FindFilamentByIndex(size_t index, FilamentColorInfo& outFilament) const; + void Clear(); + +private: + bool _loaded { false }; + + std::vector _filaments; + std::unordered_map _filamentIndexById; + std::unordered_map _filamentIndexByName; +}; + +} // namespace Slic3r diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 67744f81c6e..f97f468fe47 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -1,6 +1,7 @@ #include #include "PresetBundle.hpp" +#include "FilamentColorLibrary.hpp" #include "PrintConfig.hpp" #include "libslic3r.h" #include "Utils.hpp" @@ -57,6 +58,126 @@ void startup_profile_log(const std::string& message) BOOST_LOG_TRIVIAL(warning) << "[StartupProfile] " << message; } +std::vector SplitPrinterSetting(const AppConfig &config, const std::string &printerName, const std::string &key) +{ + std::vector values; + const std::string setting = config.get_printer_setting(printerName, key); + if (!setting.empty()) + boost::algorithm::split(values, setting, boost::algorithm::is_any_of(",")); + return values; +} + +std::vector LoadFilamentColourModes(const AppConfig &config, const std::string &printerName) +{ + std::vector modes; + const std::vector modeValues = SplitPrinterSetting(config, printerName, "filament_colour_mode"); + modes.reserve(modeValues.size()); + for (const std::string &modeValue : modeValues) + { + const bool isGradient = modeValue == std::to_string(FilamentColorModeToConfig(FilamentColorMode::Gradient)); + modes.emplace_back(isGradient ? FilamentColorMode::Gradient : FilamentColorMode::Segment); + } + return modes; +} + +std::vector BuildFilamentColors(const std::vector& colors, + const std::vector& multiColors, + const std::vector& modes, size_t targetCount) +{ + std::vector filamentColors; + filamentColors.reserve(targetCount); + + for (size_t i = 0; i < targetCount; ++i) + { + const std::string fallbackColor = i < colors.size() && !colors[i].empty() ? colors[i] : "#26A69A"; + const std::string multiColor = i < multiColors.size() ? multiColors[i] : std::string(); + FilamentColorMode colorMode = FilamentColorMode::Segment; + if (i < modes.size()) + colorMode = modes[i]; + filamentColors.emplace_back(FilamentColor::FromMultiColors(multiColor, colorMode, fallbackColor)); + } + + return filamentColors; +} + +std::vector LoadFilamentColors(const AppConfig &config, const std::string &printerName, size_t targetCount) +{ + const std::vector colors = SplitPrinterSetting(config, printerName, "filament_colors"); + const std::vector multiColors = SplitPrinterSetting(config, printerName, "filament_multi_colors"); + const std::vector modes = LoadFilamentColourModes(config, printerName); + return BuildFilamentColors(colors, multiColors, modes, targetCount); +} + +void EnsureFilamentColorFieldsAligned(DynamicPrintConfig &config) +{ + ConfigOptionStrings *filamentColor = config.option("filament_colour"); + if (filamentColor == nullptr) + return; + + const size_t targetCount = filamentColor->values.size(); + ConfigOptionStrings *multiColors = config.option("filament_multi_colors", true); + ConfigOptionInts *modes = config.option("filament_colour_mode", true); + + const size_t oldMultiCount = multiColors->values.size(); + multiColors->resize(targetCount); + modes->resize(targetCount); + + for (size_t i = 0; i < targetCount; ++i) + { + const FilamentColorMode colorMode = FilamentColorModeFromConfig(modes->values[i]); + modes->values[i] = FilamentColorModeToConfig(colorMode); + const bool singleOrEmptyMulti = multiColors->values[i].empty() || + multiColors->values[i].find('|') == std::string::npos; + const bool shouldRefreshMulti = i >= oldMultiCount || multiColors->values[i].empty() || + (colorMode == FilamentColorMode::Segment && singleOrEmptyMulti); + if (shouldRefreshMulti) + multiColors->values[i] = filamentColor->values[i]; + } +} + +void ApplyFilamentColors(DynamicPrintConfig &config, const std::vector& filamentColors) +{ + std::vector colors; + std::vector multiColors; + std::vector modes; + colors.reserve(filamentColors.size()); + multiColors.reserve(filamentColors.size()); + modes.reserve(filamentColors.size()); + + for (const FilamentColor& color : filamentColors) + { + colors.emplace_back(color.PrimaryColor("#26A69A")); + multiColors.emplace_back(color.ToMultiColorsString()); + modes.emplace_back(FilamentColorModeToConfig(color.NormalizedMode())); + } + + config.option("filament_colour")->values = colors; + config.option("filament_multi_colors", true)->values = multiColors; + config.option("filament_colour_mode", true)->values = modes; + EnsureFilamentColorFieldsAligned(config); +} + +void EraseStringOptionAt(DynamicPrintConfig &config, const std::string &key, size_t index) +{ + ConfigOptionStrings *option = config.option(key, true); + if (option != nullptr && option->values.size() > index) + option->values.erase(option->values.begin() + index); +} + +void EraseIntOptionAt(DynamicPrintConfig &config, const std::string &key, size_t index) +{ + ConfigOptionInts *option = config.option(key, true); + if (option != nullptr && option->values.size() > index) + option->values.erase(option->values.begin() + index); +} + +void EraseFilamentColorFields(DynamicPrintConfig &config, size_t index) +{ + EraseStringOptionAt(config, "filament_multi_colors", index); + EraseIntOptionAt(config, "filament_colour_mode", index); + EnsureFilamentColorFieldsAligned(config); +} + } // namespace static std::vector s_project_options { @@ -64,6 +185,8 @@ static std::vector s_project_options { "flush_volumes_matrix", // BBS "filament_colour", + "filament_multi_colors", + "filament_colour_mode", "wipe_tower_x", "wipe_tower_y", "wipe_tower_rotation_angle", @@ -156,6 +279,7 @@ PresetBundle::PresetBundle() this->printers.select_preset(0); this->project_config.apply_only(FullPrintConfig::defaults(), s_project_options); + EnsureFilamentColorFieldsAligned(this->project_config); } PresetBundle::PresetBundle(const PresetBundle &rhs) @@ -1727,13 +1851,9 @@ void PresetBundle::update_selections(AppConfig &config) break; this->filament_presets.emplace_back(remove_ini_suffix(f_name)); } - std::vector filament_colors; - auto f_colors = config.get_printer_setting(initial_printer_profile_name, "filament_colors"); - if (!f_colors.empty()) { - boost::algorithm::split(filament_colors, f_colors, boost::algorithm::is_any_of(",")); - } - filament_colors.resize(filament_presets.size(), "#26A69A"); - project_config.option("filament_colour")->values = filament_colors; + std::vector filamentColors = LoadFilamentColors(config, initial_printer_profile_name, + filament_presets.size()); + ApplyFilamentColors(project_config, filamentColors); std::vector matrix; if (config.has_printer_setting(initial_printer_profile_name, "flush_volumes_matrix")) { boost::algorithm::split(matrix, config.get_printer_setting(initial_printer_profile_name, "flush_volumes_matrix"), boost::algorithm::is_any_of("|")); @@ -1863,13 +1983,9 @@ void PresetBundle::load_selections(AppConfig &config, const PresetPreferences& p break; this->filament_presets.emplace_back(remove_ini_suffix(f_name)); } - std::vector filament_colors; - auto f_colors = config.get_printer_setting(initial_printer_profile_name, "filament_colors"); - if (!f_colors.empty()) { - boost::algorithm::split(filament_colors, f_colors, boost::algorithm::is_any_of(",")); - } - filament_colors.resize(filament_presets.size(), "#26A69A"); - project_config.option("filament_colour")->values = filament_colors; + std::vector filamentColors = LoadFilamentColors(config, initial_printer_profile_name, + filament_presets.size()); + ApplyFilamentColors(project_config, filamentColors); std::vector matrix; if (config.has_printer_setting(initial_printer_profile_name, "flush_volumes_matrix")) { boost::algorithm::split(matrix, config.get_printer_setting(initial_printer_profile_name, "flush_volumes_matrix"), boost::algorithm::is_any_of("|")); @@ -1954,10 +2070,39 @@ void PresetBundle::export_selections(AppConfig &config) config.set_printer_setting(printer_name, name, filament_presets[i]); } CNumericLocalesSetter locales_setter; - std::vector physical_filament_colors = project_config.option("filament_colour")->values; - physical_filament_colors.resize(filament_presets.size(), "#26A69A"); - std::string filament_colors = boost::algorithm::join(physical_filament_colors, ","); - config.set_printer_setting(printer_name, "filament_colors", filament_colors); + std::vector projectColors = project_config.option("filament_colour")->values; + std::vector projectMultiColors; + std::vector projectModes; + if (ConfigOptionStrings *multiColors = project_config.option("filament_multi_colors")) + projectMultiColors = multiColors->values; + if (ConfigOptionInts *modes = project_config.option("filament_colour_mode")) + { + projectModes.reserve(modes->values.size()); + for (const int modeValue : modes->values) + projectModes.emplace_back(FilamentColorModeFromConfig(modeValue)); + } + + const std::vector projectFilamentColors = + BuildFilamentColors(projectColors, projectMultiColors, projectModes, filament_presets.size()); + std::vector filamentColorValues; + std::vector filamentMultiColorValues; + std::vector filamentColourModeStrings; + filamentColorValues.reserve(projectFilamentColors.size()); + filamentMultiColorValues.reserve(projectFilamentColors.size()); + filamentColourModeStrings.reserve(projectFilamentColors.size()); + for (const FilamentColor& color : projectFilamentColors) + { + filamentColorValues.emplace_back(color.PrimaryColor("#26A69A")); + filamentMultiColorValues.emplace_back(color.ToMultiColorsString()); + filamentColourModeStrings.emplace_back(std::to_string(FilamentColorModeToConfig(color.NormalizedMode()))); + } + + const std::string filamentColors = boost::algorithm::join(filamentColorValues, ","); + const std::string filamentMultiColors = boost::algorithm::join(filamentMultiColorValues, ","); + const std::string filamentColourModes = boost::algorithm::join(filamentColourModeStrings, ","); + config.set_printer_setting(printer_name, "filament_colors", filamentColors); + config.set_printer_setting(printer_name, "filament_multi_colors", filamentMultiColors); + config.set_printer_setting(printer_name, "filament_colour_mode", filamentColourModes); std::string flush_volumes_matrix = boost::algorithm::join(project_config.option("flush_volumes_matrix")->values | boost::adaptors::transformed(static_cast(std::to_string)), "|"); @@ -1999,6 +2144,7 @@ void PresetBundle::update_num_filaments(unsigned int to_del_filament_id) ams_multi_color_filment.resize(to_del_filament_id); } + EraseFilamentColorFields(project_config, to_del_filament_id); update_multi_material_filament_presets(to_del_filament_id, old_filament_count); } @@ -2012,12 +2158,16 @@ void PresetBundle::set_num_filaments(unsigned int n, std::vector ne ConfigOptionStrings* filament_color = project_config.option("filament_colour"); filament_color->resize(n); ams_multi_color_filment.resize(n); + EnsureFilamentColorFieldsAligned(project_config); // BBS set new filament color to new_color if (old_filament_count < n) { if (!new_colors.empty()) { + ConfigOptionStrings *multi_colors = project_config.option("filament_multi_colors", true); for (int i = old_filament_count; i < n; i++) { filament_color->values[i] = new_colors[i - old_filament_count]; + multi_colors->values[i] = new_colors[i - old_filament_count]; } + EnsureFilamentColorFieldsAligned(project_config); } } update_multi_material_filament_presets(size_t(-1), size_t(old_filament_count)); @@ -2034,13 +2184,17 @@ void PresetBundle::set_num_filaments(unsigned int n, std::string new_color) ConfigOptionStrings* filament_color = project_config.option("filament_colour"); filament_color->resize(n); ams_multi_color_filment.resize(n); + EnsureFilamentColorFieldsAligned(project_config); //BBS set new filament color to new_color if (old_filament_count < n) { if (!new_color.empty()) { + ConfigOptionStrings *multi_colors = project_config.option("filament_multi_colors", true); for (int i = old_filament_count; i < n; i++) { filament_color->values[i] = new_color; + multi_colors->values[i] = new_color; } + EnsureFilamentColorFieldsAligned(project_config); } } @@ -2057,7 +2211,7 @@ unsigned int PresetBundle::sync_ams_list(unsigned int &unknowns) auto filament_id = ams.opt_string("filament_id", 0u); auto filament_color = ams.opt_string("filament_colour", 0u); auto filament_changed = !ams.has("filament_changed") || ams.opt_bool("filament_changed"); - auto filament_multi_color = ams.opt("filament_multi_colors")->values; + std::vector filament_multi_color = ams.opt("filament_multi_colors")->values; if (filament_id.empty()) continue; if (!filament_changed && this->filament_presets.size() > filament_presets.size()) { filament_presets.push_back(this->filament_presets[filament_presets.size()]); @@ -2104,6 +2258,7 @@ unsigned int PresetBundle::sync_ams_list(unsigned int &unknowns) ConfigOptionStrings *filament_color = project_config.option("filament_colour"); filament_color->resize(filament_presets.size()); filament_color->values = filament_colors; + EnsureFilamentColorFieldsAligned(project_config); update_multi_material_filament_presets(); return filament_presets.size(); } @@ -2820,6 +2975,7 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool // 4) Load the project config values (the per extruder wipe matrix etc). this->project_config.apply_only(config, s_project_options); + EnsureFilamentColorFieldsAligned(this->project_config); break; } diff --git a/src/libslic3r/PresetBundle.hpp b/src/libslic3r/PresetBundle.hpp index 0ae15163455..075b32e3c50 100644 --- a/src/libslic3r/PresetBundle.hpp +++ b/src/libslic3r/PresetBundle.hpp @@ -3,12 +3,14 @@ #include "Preset.hpp" #include "AppConfig.hpp" +#include "FilamentColorLibrary.hpp" #include "enum_bitmask.hpp" #include "MixedFilament.hpp" #include #include #include +#include #include #define DEFAULT_USER_FOLDER_NAME "default" @@ -33,6 +35,8 @@ struct ConnectMachineInfo std::string filament_info {""}; std::string nozzle_info {""}; std::string color_info{""}; + std::vector multiColors; + Slic3r::FilamentColorMode colorMode { Slic3r::FilamentColorMode::Segment }; int index {0}; }; diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index eab14f0d449..27e0a48831c 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -564,6 +564,8 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n "fan_speedup_overhangs", "fan_speedup_time", "filament_colour", + "filament_multi_colors", + "filament_colour_mode", "default_filament_colour", "filament_diameter", "filament_density", diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index ac67d087502..78056a4ccb3 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -2060,6 +2060,22 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionStrings{ "#F2754E" }); + def = this->add("filament_multi_colors", coStrings); + def->label = L("Filament multi colors"); + def->tooltip = L("Serialized filament color sequence. Multiple colors are separated by '|'."); + def->mode = comAdvanced; + def->cli = ConfigOptionDef::nocli; + def->set_default_value(new ConfigOptionStrings{ "" }); + + def = this->add("filament_colour_mode", coInts); + def->label = L("Filament color display mode"); + def->tooltip = L("Filament color display mode: 0 for split colors, 1 for gradient."); + def->mode = comAdvanced; + def->cli = ConfigOptionDef::nocli; + def->min = 0; + def->max = 1; + def->set_default_value(new ConfigOptionInts{ 0 }); + def = this->add("thumb0", coStrings); def->label = L("small thumb"); def->tooltip = L("first small thumb"); @@ -6443,6 +6459,7 @@ void PrintConfigDef::init_filament_option_keys() "retraction_length", "z_hop", "z_hop_types", "retract_lift_above", "retract_lift_below", "retract_lift_enforce", "retraction_speed", "deretraction_speed", "retract_before_wipe", "retract_restart_extra", "retraction_minimum_travel", "wipe", "wipe_distance", "retract_when_changing_layer", "retract_length_toolchange", "retract_restart_extra_toolchange", "filament_colour", + "filament_multi_colors", "filament_colour_mode", "default_filament_profile","retraction_distances_when_cut","long_retractions_when_cut"/*,"filament_seam_gap"*/ }; diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 0143f1f1e43..24f44fbb72a 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1337,6 +1337,8 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionBools, dont_slow_down_outer_wall)) ((ConfigOptionFloats, fan_cooling_layer_time)) ((ConfigOptionStrings, filament_colour)) + ((ConfigOptionStrings, filament_multi_colors)) + ((ConfigOptionInts, filament_colour_mode)) ((ConfigOptionBools, activate_air_filtration)) ((ConfigOptionInts, during_print_exhaust_fan_speed)) ((ConfigOptionInts, complete_print_exhaust_fan_speed)) diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 9b70587c054..411c1c720d9 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -109,6 +109,8 @@ set(SLIC3R_GUI_SOURCES GUI/ExtrusionCalibration.hpp GUI/Field.cpp GUI/Field.hpp + GUI/FilamentColorUtils.cpp + GUI/FilamentColorUtils.hpp GUI/FileArchiveDialog.cpp GUI/FileArchiveDialog.hpp GUI/format.hpp @@ -325,6 +327,8 @@ set(SLIC3R_GUI_SOURCES GUI/MixedFilamentBadge.hpp GUI/ObjColorDialog.cpp GUI/ObjColorDialog.hpp + GUI/FilamentColorDialog.cpp + GUI/FilamentColorDialog.hpp GUI/ObjectDataViewModel.cpp GUI/ObjectDataViewModel.hpp GUI/OG_CustomCtrl.cpp diff --git a/src/slic3r/GUI/FilamentColorDialog.cpp b/src/slic3r/GUI/FilamentColorDialog.cpp new file mode 100644 index 00000000000..f6b17f98052 --- /dev/null +++ b/src/slic3r/GUI/FilamentColorDialog.cpp @@ -0,0 +1,912 @@ +#include "FilamentColorDialog.hpp" + +#include "FilamentColorUtils.hpp" +#include "GUI_App.hpp" +#include "Widgets/Button.hpp" +#include "Widgets/Label.hpp" +#include "Widgets/StateColor.hpp" +#include "Widgets/StaticBox.hpp" +#include "wxExtensions.hpp" +#include "libslic3r/AppConfig.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace Slic3r +{ +namespace GUI +{ +namespace +{ + +/** + * @brief HSV value cached for color sorting. + */ +struct SortHsv +{ + float h { 0.0f }; + float s { 0.0f }; + float v { 0.0f }; +}; + +/** + * @brief Lightweight color item with cached sort data. + */ +struct ColorSortItem +{ + const FilamentColorItem* colorItem { nullptr }; + std::vector colors; + std::vector hsvColors; + int typeOrder { 0 }; + std::string enName; + std::string zhName; +}; + +/** + * @brief Gets the current language code used by the filament color data. + */ +std::string GetLanguageCode() +{ + if (wxGetApp().app_config == nullptr) + return "en"; + + const std::string language = wxGetApp().app_config->get("language"); + if (language.empty()) + return "en"; + if (language == "zh-cn") + return "zh_CN"; + return language; +} + +/** + * @brief Converts an UTF-8 string to wxString. + */ +wxString FromStdString(const std::string& value) +{ + return wxString::FromUTF8(value.c_str()); +} + +/** + * @brief Gets a localized color name with English fallback. + */ +std::string GetLocalizedColorName(const FilamentColorItem& colorItem, const std::string& languageCode, + const std::vector& colors) +{ + std::unordered_map::const_iterator localizedName = + colorItem.colorNames.find(languageCode); + if (localizedName != colorItem.colorNames.end() && !localizedName->second.empty()) + return localizedName->second; + + std::unordered_map::const_iterator englishName = colorItem.colorNames.find("en"); + if (englishName != colorItem.colorNames.end() && !englishName->second.empty()) + return englishName->second; + + if (colors.size() > 1) + return _L("Multiple Color").ToStdString(wxConvUTF8); + return colors.empty() ? std::string() : colors.front(); +} + +/** + * @brief Gets normalized colors already loaded by the filament color library. + */ +std::vector ColorListFromLibrary(const FilamentColorItem& colorItem) +{ + return colorItem.colorData.colors; +} + +/** + * @brief Builds color data from a built-in color. + */ +bool MakeFilamentColor(const FilamentColorItem& colorItem, FilamentColor& colorData) +{ + if (colorItem.colorData.colors.empty()) + { + BOOST_LOG_TRIVIAL(error) << "Invalid official filament color without valid colors: " << colorItem.sku; + return false; + } + + colorData = FilamentColor::FromColors(colorItem.colorData.colors, colorItem.colorData.mode); + return true; +} + +/** + * @brief Finds an official color item matching the saved colors and display mode. + */ +std::vector::const_iterator FindColorBySavedColors(const FilamentColorInfo& filament, + const FilamentColor& colorData) +{ + if (colorData.colors.empty()) + return filament.colors.end(); + + const FilamentColor normalizedColor = FilamentColor::FromColors(colorData.colors, colorData.mode); + return std::find_if(filament.colors.begin(), filament.colors.end(), + [&normalizedColor](const FilamentColorItem& item) + { + return item.colorData.Matches(normalizedColor); + }); +} + +/** + * @brief Gets the preview name for the current selected color data. + */ +std::string GetSelectionDisplayName(const FilamentColorInfo& filament, const FilamentColor& colorData, + const std::string& selectedSku, const std::string& languageCode) +{ + if (!selectedSku.empty()) + { + std::vector::const_iterator colorIt = + std::find_if(filament.colors.begin(), filament.colors.end(), + [&selectedSku](const FilamentColorItem& colorItem) + { + return colorItem.sku == selectedSku; + }); + if (colorIt != filament.colors.end()) + { + const std::vector officialColors = ColorListFromLibrary(*colorIt); + const std::string name = GetLocalizedColorName(*colorIt, languageCode, officialColors); + if (!name.empty()) + return name; + } + } + + if (colorData.colors.size() > 1) + return _L("Multiple Color").ToStdString(wxConvUTF8); + return colorData.PrimaryColor("#26A69A"); +} + +/** + * @brief Converts a color to HSV for sorting. + */ +SortHsv MakeSortHsvFromHex(const std::string& color) +{ + wxColour wxColor(color); + if (!wxColor.IsOk()) + return SortHsv(); + + SortHsv hsv; + RGB2HSV(static_cast(wxColor.Red()) / 255.0f, static_cast(wxColor.Green()) / 255.0f, + static_cast(wxColor.Blue()) / 255.0f, &hsv.h, &hsv.s, &hsv.v); + if (hsv.h < 0.0f) + hsv.h += 360.0f; + return hsv; +} + +/** + * @brief Gets the display type order used by the color grid. + */ +int GetColorTypeOrder(const std::vector& colors, FilamentColorMode mode) +{ + if (colors.size() <= 1) + return 0; + return mode == FilamentColorMode::Gradient ? 2 : 1; +} + +/** + * @brief Compares two HSV values. + */ +int CompareHsv(const SortHsv& left, const SortHsv& right) +{ + if (left.h != right.h) + return left.h < right.h ? -1 : 1; + if (left.s != right.s) + return left.s < right.s ? -1 : 1; + if (left.v != right.v) + return left.v < right.v ? -1 : 1; + return 0; +} + +/** + * @brief Gets a language-specific name used as the final sort tie breaker. + */ +std::string GetColorNameForSort(const FilamentColorItem& colorItem, const std::string& languageCode) +{ + std::unordered_map::const_iterator name = colorItem.colorNames.find(languageCode); + return name != colorItem.colorNames.end() ? name->second : std::string(); +} + +/** + * @brief Builds a lightweight, precomputed sort key. + */ +ColorSortItem MakeColorSortItem(const FilamentColorItem& colorItem) +{ + ColorSortItem item; + item.colorItem = &colorItem; + item.colors = ColorListFromLibrary(colorItem); + item.typeOrder = GetColorTypeOrder(item.colors, colorItem.colorData.NormalizedMode()); + item.hsvColors.reserve(item.colors.size()); + for (const std::string& hexColor : item.colors) + item.hsvColors.emplace_back(MakeSortHsvFromHex(hexColor)); + item.enName = GetColorNameForSort(colorItem, "en"); + item.zhName = GetColorNameForSort(colorItem, "zh_CN"); + return item; +} + +/** + * @brief Compares two precomputed sort keys. + */ +bool ColorSortItemLess(const ColorSortItem& left, const ColorSortItem& right) +{ + if (left.colorItem == nullptr) + return false; + if (right.colorItem == nullptr) + return true; + + if (left.typeOrder != right.typeOrder) + return left.typeOrder < right.typeOrder; + + const size_t color_count = std::min(left.hsvColors.size(), right.hsvColors.size()); + for (size_t index = 0; index < color_count; ++index) + { + const int hsv_compare = CompareHsv(left.hsvColors[index], right.hsvColors[index]); + if (hsv_compare != 0) + return hsv_compare < 0; + } + + if (left.colors.size() != right.colors.size()) + return left.colors.size() < right.colors.size(); + if (left.colorItem->sku != right.colorItem->sku) + return left.colorItem->sku < right.colorItem->sku; + if (left.enName != right.enName) + return left.enName < right.enName; + return left.zhName < right.zhName; +} + +/** + * @brief Builds sorted lightweight items without copying full color records. + */ +std::vector BuildSortedColorItems(const std::vector& colorItems) +{ + std::vector items; + items.reserve(colorItems.size()); + for (const FilamentColorItem& colorItem : colorItems) + items.emplace_back(MakeColorSortItem(colorItem)); + + std::stable_sort(items.begin(), items.end(), ColorSortItemLess); + return items; +} + +/** + * @brief Gets the light swatch border color used by the official color dialog. + */ +wxColour FilamentColorLightBorderColor() +{ + return StateColor::darkModeColorFor(wxColour(180, 180, 180)); +} + +wxBitmap MakePreviewBitmap(const FilamentColor& colorData, int size); + +wxFont DialogFont(wxFontWeight weight) +{ + wxFont font = Label::Body_14; + font.SetWeight(weight); + return font; +} + +/** + * @brief Small swatch used by the filament color dialog. + */ +class FilamentColorSwatch : public wxPanel +{ +public: + FilamentColorSwatch(wxWindow* parent, const FilamentColorItem& colorItem, + const std::string& languageCode, const wxSize& size) + : wxPanel(parent, wxID_ANY, wxDefaultPosition, size) + , _colorItem(&colorItem) + , _languageCode(languageCode) + { + SetMinSize(size); + SetMaxSize(size); + SetBackgroundStyle(wxBG_STYLE_PAINT); + SetCursor(wxCursor(wxCURSOR_HAND)); + const std::vector colors = ColorListFromLibrary(colorItem); + const std::string tooltip = GetLocalizedColorName(colorItem, languageCode, colors); + if (!tooltip.empty()) + SetToolTip(FromStdString(tooltip)); + Bind(wxEVT_PAINT, &FilamentColorSwatch::OnPaint, this); + } + + /** + * @brief Updates the selected visual state. + */ + void SetSelected(bool selected) + { + if (_selected == selected) + return; + _selected = selected; + Refresh(); + } + +private: + void OnPaint(wxPaintEvent&) + { + wxAutoBufferedPaintDC dc(this); + const wxSize size = GetClientSize(); + dc.SetBackground(wxBrush(StateColor::darkModeColorFor(wxColour("#FFFFFF")))); + dc.Clear(); + + if (_colorItem == nullptr) + return; + + const int selectedBorderWidth = std::max(FromDIP(1), 2); + const int selectedGap = FromDIP(1); + const int fillInset = selectedBorderWidth + selectedGap; + const int fillWidth = std::max(1, size.GetWidth() - fillInset * 2); + const int fillHeight = std::max(1, size.GetHeight() - fillInset * 2); + const wxRect fillRect(fillInset, fillInset, fillWidth, fillHeight); + FilamentColor colorData; + if (!MakeFilamentColor(*_colorItem, colorData)) + return; + + const int bitmapWidth = fillRect.GetWidth(); + const int bitmapHeight = fillRect.GetHeight(); + const wxColour borderColor = FilamentColorLightBorderColor(); + wxBitmap* bitmap = FilamentColorUtils::GetFilamentColorIcon(colorData.colors, colorData.NormalizedMode(), "", + bitmapWidth, bitmapHeight, borderColor); + if (bitmap != nullptr) + dc.DrawBitmap(*bitmap, fillRect.GetLeft(), fillRect.GetTop(), true); + + if (_selected) + { + const int middleHeight = std::max(0, size.GetHeight() - selectedBorderWidth * 2); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.SetBrush(wxBrush(wxColour("#019687"))); + dc.DrawRectangle(0, 0, size.GetWidth(), selectedBorderWidth); + dc.DrawRectangle(0, selectedBorderWidth, selectedBorderWidth, middleHeight); + dc.DrawRectangle(size.GetWidth() - selectedBorderWidth, selectedBorderWidth, + selectedBorderWidth, middleHeight); + dc.DrawRectangle(0, size.GetHeight() - selectedBorderWidth, size.GetWidth(), selectedBorderWidth); + } + } + +private: + const FilamentColorItem* _colorItem { nullptr }; + std::string _languageCode; + bool _selected { false }; +}; + +/** + * @brief Tertiary button used for the custom color entry. + */ +class MoreColorPanel : public wxPanel +{ +public: + MoreColorPanel(wxWindow* parent, const wxSize& size) + : wxPanel(parent, wxID_ANY, wxDefaultPosition, size) + , _icon(this, "filament_color_more", 16) + { + SetMinSize(size); + SetBackgroundStyle(wxBG_STYLE_PAINT); + SetCursor(wxCursor(wxCURSOR_HAND)); + Bind(wxEVT_PAINT, &MoreColorPanel::OnPaint, this); + } + +private: + void OnPaint(wxPaintEvent&) + { + wxAutoBufferedPaintDC dc(this); + const wxSize size = GetClientSize(); + dc.SetBackground(wxBrush(StateColor::darkModeColorFor(wxColour("#F0F0F0")))); + dc.Clear(); + + dc.SetFont(DialogFont(wxFONTWEIGHT_MEDIUM)); + dc.SetTextForeground(StateColor::darkModeColorFor(wxColour("#242424"))); + const wxString label = _L("Other Colors"); + wxCoord textWidth = 0; + wxCoord textHeight = 0; + dc.GetTextExtent(label, &textWidth, &textHeight); + const int iconWidth = _icon.bmp().IsOk() ? _icon.GetBmpWidth() : FromDIP(16); + const int iconHeight = _icon.bmp().IsOk() ? _icon.GetBmpHeight() : FromDIP(16); + const int gap = FromDIP(4); + const int contentWidth = iconWidth + gap + static_cast(textWidth); + const int iconX = std::max(0, (size.GetWidth() - contentWidth) / 2); + const int iconY = std::max(0, (size.GetHeight() - iconHeight) / 2); + const int textX = iconX + iconWidth + gap; + const int textY = std::max(0, (size.GetHeight() - static_cast(textHeight)) / 2); + + if (_icon.bmp().IsOk()) + { + dc.DrawBitmap(_icon.bmp(), iconX, iconY, true); + } + else + { + dc.SetPen(wxPen(StateColor::darkModeColorFor(wxColour("#8A8A8A")), FromDIP(2))); + dc.DrawLine(iconX + iconWidth / 2, iconY + FromDIP(4), + iconX + iconWidth / 2, iconY + iconHeight - FromDIP(4)); + dc.DrawLine(iconX + FromDIP(4), iconY + iconHeight / 2, + iconX + iconWidth - FromDIP(4), iconY + iconHeight / 2); + } + dc.DrawText(label, textX, textY); + } + +private: + ScalableBitmap _icon; +}; + +wxBitmap MakePreviewBitmap(const FilamentColor& colorData, int size) +{ + size = std::max(1, size); + wxBitmap output(size, size); + wxMemoryDC dc; + dc.SelectObject(output); + dc.SetBackground(wxBrush(StateColor::darkModeColorFor(wxColour("#FFFFFF")))); + dc.Clear(); + + const wxColour borderColor = FilamentColorLightBorderColor(); + const FilamentColorMode colorMode = colorData.NormalizedMode(); + wxBitmap* bitmap = FilamentColorUtils::GetFilamentColorIcon(colorData.colors, colorMode, "", + size, size, borderColor); + if (bitmap != nullptr) + dc.DrawBitmap(*bitmap, 0, 0, true); + + dc.SelectObject(wxNullBitmap); + return output; +} + +wxRegion MakeRoundedRegion(const wxSize& size, int radius) +{ + const int width = std::max(1, size.GetWidth()); + const int height = std::max(1, size.GetHeight()); + radius = std::max(0, std::min(radius, std::min(width, height) / 2)); + if (radius == 0) + return wxRegion(0, 0, width, height); + + constexpr double pi = 3.14159265358979323846; + const int segments = 8; + std::vector points; + points.reserve((segments + 1) * 4); + + auto appendArc = [&points, radius, segments, pi](int centerX, int centerY, double start, double end) + { + for (int index = 0; index <= segments; ++index) + { + const double ratio = static_cast(index) / static_cast(segments); + const double angle = (start + (end - start) * ratio) * pi / 180.0; + const int x = centerX + static_cast(std::round(std::cos(angle) * radius)); + const int y = centerY + static_cast(std::round(std::sin(angle) * radius)); + points.emplace_back(x, y); + } + }; + + appendArc(width - radius - 1, radius, -90.0, 0.0); + appendArc(width - radius - 1, height - radius - 1, 0.0, 90.0); + appendArc(radius, height - radius - 1, 90.0, 180.0); + appendArc(radius, radius, 180.0, 270.0); + return wxRegion(static_cast(points.size()), points.data()); +} + +bool IsValidWindowRect(const wxRect& rect) +{ + return rect.GetWidth() > 0 && rect.GetHeight() > 0; +} + +wxRect WindowClientScreenRect(const wxWindow* window) +{ + if (window == nullptr) + return wxRect(); + + const wxSize size = window->GetClientSize(); + if (size.GetWidth() <= 0 || size.GetHeight() <= 0) + return wxRect(); + + return wxRect(window->ClientToScreen(wxPoint(0, 0)), size); +} + +} // namespace + +FilamentColorDialog::FilamentColorDialog(wxWindow* parent, const FilamentColorInfo& filament, + const FilamentColor& currentColor) + : DPIDialog(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE | wxFRAME_SHAPED) + , _filament(filament) + , _languageCode(GetLanguageCode()) +{ + const FilamentColor normalizedCurrent = + FilamentColor::FromColors(currentColor.colors, currentColor.mode, "#26A69A"); + std::vector::const_iterator currentColorIt = + FindColorBySavedColors(_filament, normalizedCurrent); + + if (currentColorIt != _filament.colors.end() && MakeFilamentColor(*currentColorIt, _selection)) + { + _selectedSku = currentColorIt->sku; + _highlightSku = currentColorIt->sku; + } + else + { + _selectedSku.clear(); + _highlightSku.clear(); + _selection = normalizedCurrent; + } + + BuildUi(); + UpdatePreview(); + UpdateSwatchSelection(); +} + +void FilamentColorDialog::BuildUi() +{ + SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#FFFFFF"))); + + wxBoxSizer* root = new wxBoxSizer(wxVERTICAL); + const int dialogWidth = FromDIP(380); + const int contentMargin = FromDIP(16); + const int buttonMargin = FromDIP(20); + const int contentWidth = dialogWidth - contentMargin * 2; + + StaticBox* card = new StaticBox(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE); + card->SetBackgroundColorNormal(wxColour("#FFFFFF")); + card->SetCornerRadius(FromDIP(8)); + wxBoxSizer* cardSizer = new wxBoxSizer(wxVERTICAL); + + wxBoxSizer* currentRow = new wxBoxSizer(wxHORIZONTAL); + _previewBitmap = new wxStaticBitmap(card, wxID_ANY, wxBitmap(FromDIP(60), FromDIP(60))); + BindDragWindow(_previewBitmap); + currentRow->Add(_previewBitmap, 0, wxALIGN_TOP | wxRIGHT, FromDIP(12)); + + wxBoxSizer* infoSizer = new wxBoxSizer(wxVERTICAL); + _nameLabel = new wxStaticText(card, wxID_ANY, wxEmptyString); + _nameLabel->SetFont(DialogFont(wxFONTWEIGHT_SEMIBOLD)); + _nameLabel->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#242424"))); + _nameLabel->SetMinSize(wxSize(-1, FromDIP(20))); + BindDragWindow(_nameLabel); + _skuLabel = new wxStaticText(card, wxID_ANY, wxEmptyString); + _skuLabel->SetFont(DialogFont(wxFONTWEIGHT_MEDIUM)); + _skuLabel->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#4A4A4A"))); + _skuLabel->SetMinSize(wxSize(-1, FromDIP(20))); + BindDragWindow(_skuLabel); + infoSizer->Add(_nameLabel, 0, wxBOTTOM, FromDIP(6)); + infoSizer->Add(_skuLabel, 0); + currentRow->Add(infoSizer, 1, wxALIGN_TOP); + cardSizer->AddSpacer(FromDIP(28)); + cardSizer->Add(currentRow, 0, wxEXPAND | wxLEFT | wxRIGHT, contentMargin); + + wxStaticText* officialLabel = new wxStaticText(card, wxID_ANY, _L("Official Filaments")); + officialLabel->SetFont(DialogFont(wxFONTWEIGHT_MEDIUM)); + officialLabel->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#242424"))); + officialLabel->SetMinSize(wxSize(-1, FromDIP(20))); + BindDragWindow(officialLabel); + cardSizer->AddSpacer(FromDIP(20)); + cardSizer->Add(officialLabel, 0, wxLEFT | wxRIGHT, contentMargin); + + wxPanel* swatchPanel = new wxPanel(card, wxID_ANY); + swatchPanel->SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#FFFFFF"))); + const int swatchColumns = 10; + const int swatchColorSize = FromDIP(24); + const int swatchFrameInset = FromDIP(3); + const wxSize swatchSize(swatchColorSize + swatchFrameInset * 2, swatchColorSize + swatchFrameInset * 2); + const int swatchHGap = FromDIP(5); + const int swatchVGap = 0; + wxGridSizer* swatchSizer = new wxGridSizer(0, swatchColumns, swatchVGap, swatchHGap); + const std::vector sortedItems = BuildSortedColorItems(_filament.colors); + for (const ColorSortItem& item : sortedItems) + { + if (item.colorItem == nullptr) + continue; + + const FilamentColorItem* colorItem = item.colorItem; + FilamentColorSwatch* swatch = new FilamentColorSwatch(swatchPanel, *colorItem, _languageCode, swatchSize); + swatch->Bind(wxEVT_LEFT_UP, [this, colorItem](wxMouseEvent&) + { + if (colorItem != nullptr) + SelectFilamentColor(*colorItem); + }); + swatchSizer->Add(swatch, 0); + _swatchBySku.emplace_back(swatch, colorItem->sku); + } + const size_t swatchCount = _swatchBySku.size(); + const size_t swatchColumnCount = static_cast(swatchColumns); + int swatchRows = 0; + if (swatchCount > 0) + swatchRows = static_cast((swatchCount + swatchColumnCount - 1) / swatchColumnCount); + const int swatchVisibleRows = std::max(2, swatchRows); + const int swatchPanelHeight = swatchVisibleRows * swatchSize.GetHeight() + (swatchVisibleRows - 1) * swatchVGap; + swatchPanel->SetMinSize(wxSize(contentWidth, swatchPanelHeight)); + swatchPanel->SetSizer(swatchSizer); + cardSizer->AddSpacer(FromDIP(20)); + cardSizer->Add(swatchPanel, 0, wxLEFT | wxRIGHT, contentMargin); + + MoreColorPanel* moreButton = new MoreColorPanel(card, wxSize(contentWidth, FromDIP(40))); + moreButton->Bind(wxEVT_LEFT_UP, [this](wxMouseEvent&) + { + OpenMoreColorDialog(); + }); + cardSizer->AddSpacer(FromDIP(20)); + cardSizer->Add(moreButton, 0, wxLEFT | wxRIGHT, contentMargin); + + cardSizer->AddSpacer(FromDIP(16)); + wxPanel* divider = new wxPanel(card, wxID_ANY); + divider->SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#F0F0F0"))); + divider->SetMinSize(wxSize(dialogWidth, FromDIP(1))); + BindDragWindow(divider); + cardSizer->Add(divider, 0, wxEXPAND); + + wxPanel* buttonPanel = new wxPanel(card, wxID_ANY); + buttonPanel->SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#FFFFFF"))); + buttonPanel->SetMinSize(wxSize(dialogWidth, FromDIP(61))); + BindDragWindow(buttonPanel); + wxBoxSizer* buttonPanelSizer = new wxBoxSizer(wxVERTICAL); + wxBoxSizer* buttons = new wxBoxSizer(wxHORIZONTAL); + Button* cancel = new Button(buttonPanel, _L("Cancel"), wxEmptyString, wxBORDER_NONE, 0, wxID_CANCEL); + cancel->SetStyle(ButtonStyle::Regular, ButtonType::Choice); + const wxSize buttonSize(FromDIP(166), FromDIP(38)); + cancel->SetMinSize(buttonSize); + cancel->SetSize(buttonSize); + cancel->SetCornerRadius(FromDIP(4)); + cancel->SetBackgroundColorNormal(wxColour("#FFFFFF")); + cancel->SetBorderColorNormal(wxColour("#D1D5DC")); + cancel->SetTextColorNormal(wxColour("#242424")); + cancel->SetFont(DialogFont(wxFONTWEIGHT_MEDIUM)); + Button* ok = new Button(buttonPanel, _L("OK"), wxEmptyString, wxBORDER_NONE, 0, wxID_OK); + ok->SetStyle(ButtonStyle::Confirm, ButtonType::Choice); + ok->SetMinSize(buttonSize); + ok->SetSize(buttonSize); + ok->SetCornerRadius(FromDIP(4)); + ok->SetBackgroundColorNormal(wxColour("#019687")); + ok->SetBorderColorNormal(wxColour("#019687")); + ok->SetTextColorNormal(wxColour("#FFFFFF")); + ok->SetFont(DialogFont(wxFONTWEIGHT_MEDIUM)); + + buttons->Add(cancel, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(8)); + buttons->Add(ok, 0, wxALIGN_CENTER_VERTICAL); + buttonPanelSizer->AddSpacer(FromDIP(12)); + buttonPanelSizer->Add(buttons, 0, wxLEFT | wxRIGHT, buttonMargin); + buttonPanel->SetSizer(buttonPanelSizer); + cardSizer->Add(buttonPanel, 0, wxEXPAND); + + cancel->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) + { + EndModal(wxID_CANCEL); + }); + + ok->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) + { + EndModal(wxID_OK); + }); + + card->SetSizer(cardSizer); + BindDragWindow(card); + BindDragWindow(this); + root->Add(card, 0, wxEXPAND); + SetSizer(root); + root->SetSizeHints(this); + SetClientSize(wxSize(dialogWidth, root->CalcMin().GetHeight())); + SetMinClientSize(wxSize(dialogWidth, root->CalcMin().GetHeight())); + UpdateRoundedShape(); + Layout(); + PlaceNearFilamentPanel(); +} + +void FilamentColorDialog::SelectFilamentColor(const FilamentColorItem& colorItem) +{ + FilamentColor colorData; + if (!MakeFilamentColor(colorItem, colorData)) + return; + + _selection = colorData; + _selectedSku = colorItem.sku; + _highlightSku = colorItem.sku; + UpdatePreview(); + UpdateSwatchSelection(); +} + +void FilamentColorDialog::SelectCustomColor(const std::string& color) +{ + const std::string normalized = FilamentColorUtils::NormalizeHexColor(color, "#26A69A"); + _selection = FilamentColor::FromMultiColors(normalized, FilamentColorMode::Segment, normalized); + _selectedSku.clear(); + _highlightSku.clear(); + UpdatePreview(); + UpdateSwatchSelection(); +} + +void FilamentColorDialog::UpdatePreview() +{ + if (_previewBitmap == nullptr || _nameLabel == nullptr || _skuLabel == nullptr) + return; + + _previewBitmap->SetBitmap(MakePreviewBitmap(_selection, FromDIP(60))); + + const wxString name = FromStdString(GetSelectionDisplayName(_filament, _selection, _selectedSku, _languageCode)); + _nameLabel->SetLabel(name); + _skuLabel->SetLabel(_selectedSku.empty() ? wxEmptyString : FromStdString("sku " + _selectedSku)); + _skuLabel->Show(!_selectedSku.empty()); + Layout(); +} + +void FilamentColorDialog::UpdateSwatchSelection() +{ + for (const std::pair& item : _swatchBySku) + { + FilamentColorSwatch* swatch = dynamic_cast(item.first); + if (swatch != nullptr) + swatch->SetSelected(!_highlightSku.empty() && item.second == _highlightSku); + } +} + +void FilamentColorDialog::on_dpi_changed(const wxRect&) +{ + Fit(); + UpdateRoundedShape(); + PlaceNearFilamentPanel(); +} + +void FilamentColorDialog::UpdateRoundedShape() +{ + const wxSize size = GetSize(); + if (size.GetWidth() <= 0 || size.GetHeight() <= 0) + return; + + SetShape(MakeRoundedRegion(size, FromDIP(8))); +} + +void FilamentColorDialog::BindDragWindow(wxWindow* window) +{ + if (window == nullptr) + return; + + window->Bind(wxEVT_LEFT_DOWN, &FilamentColorDialog::StartDrag, this); + window->Bind(wxEVT_MOTION, &FilamentColorDialog::DragDialog, this); + window->Bind(wxEVT_LEFT_UP, &FilamentColorDialog::EndDrag, this); +} + +void FilamentColorDialog::StartDrag(wxMouseEvent& event) +{ + _dragPending = true; + _isDragging = false; + _dragStartMouse = wxGetMousePosition(); + _dragStartPosition = GetScreenPosition(); + event.Skip(); +} + +void FilamentColorDialog::DragDialog(wxMouseEvent& event) +{ + if (!_dragPending && !_isDragging) + { + event.Skip(); + return; + } + + if (!event.LeftIsDown()) + { + _dragPending = false; + _isDragging = false; + if (HasCapture()) + ReleaseMouse(); + event.Skip(); + return; + } + + const wxPoint currentMouse = wxGetMousePosition(); + if (!_isDragging) + { + const int threshold = FromDIP(3); + const int distanceX = std::abs(currentMouse.x - _dragStartMouse.x); + const int distanceY = std::abs(currentMouse.y - _dragStartMouse.y); + if (distanceX < threshold && distanceY < threshold) + { + event.Skip(); + return; + } + + _isDragging = true; + if (!HasCapture()) + CaptureMouse(); + } + + if (!_isDragging) + { + event.Skip(); + return; + } + + Move(wxPoint(_dragStartPosition.x + currentMouse.x - _dragStartMouse.x, + _dragStartPosition.y + currentMouse.y - _dragStartMouse.y)); +} + +void FilamentColorDialog::EndDrag(wxMouseEvent& event) +{ + if (_dragPending || _isDragging) + { + _dragPending = false; + _isDragging = false; + if (HasCapture()) + ReleaseMouse(); + } + event.Skip(); +} + +void FilamentColorDialog::PlaceNearFilamentPanel() +{ + wxWindow* parent = GetParent(); + wxWindow* topWindow = parent != nullptr ? wxGetTopLevelParent(parent) : nullptr; + const wxRect parentRect = WindowClientScreenRect(parent); + const wxRect topRect = WindowClientScreenRect(topWindow); + const wxSize dialogSize = GetSize(); + + wxRect horizontalAnchor = parentRect; + wxRect verticalAnchor = parentRect; + const int maxAnchorRight = IsValidWindowRect(topRect) ? topRect.GetLeft() + topRect.GetWidth() / 2 : 0; + const int minPanelWidth = FromDIP(220); + const int minPanelHeight = FromDIP(80); + + for (wxWindow* window = parent; window != nullptr && window != topWindow; window = window->GetParent()) + { + const wxRect rect = WindowClientScreenRect(window); + if (!IsValidWindowRect(rect)) + continue; + + const bool isLeftPanel = !IsValidWindowRect(topRect) || rect.GetRight() <= maxAnchorRight; + if (isLeftPanel && (!IsValidWindowRect(horizontalAnchor) || rect.GetRight() >= horizontalAnchor.GetRight())) + horizontalAnchor = rect; + + const bool containsParent = IsValidWindowRect(parentRect) && + rect.GetTop() <= parentRect.GetTop() && rect.GetBottom() >= parentRect.GetBottom(); + if (containsParent && rect.GetWidth() >= minPanelWidth && rect.GetHeight() >= minPanelHeight) + { + verticalAnchor = rect; + break; + } + } + + const wxPoint fallbackPosition = topWindow != nullptr ? topWindow->ClientToScreen(wxPoint(0, 0)) : wxPoint(0, 0); + int x = IsValidWindowRect(horizontalAnchor) ? horizontalAnchor.GetRight() + FromDIP(8) : fallbackPosition.x; + int y = IsValidWindowRect(verticalAnchor) ? verticalAnchor.GetTop() : fallbackPosition.y + FromDIP(100); + + const int displayIndex = wxDisplay::GetFromWindow(parent != nullptr ? parent : this); + const wxDisplay display(displayIndex == wxNOT_FOUND ? 0 : static_cast(displayIndex)); + const wxRect area = display.IsOk() ? display.GetClientArea() : wxRect(fallbackPosition, wxSize(1920, 1080)); + x = std::max(area.GetLeft() + FromDIP(8), std::min(x, area.GetRight() - dialogSize.GetWidth() - FromDIP(8))); + y = std::max(area.GetTop() + FromDIP(8), std::min(y, area.GetBottom() - dialogSize.GetHeight() - FromDIP(8))); + Move(wxPoint(x, y)); +} + +void FilamentColorDialog::OpenMoreColorDialog() +{ + wxColourData data; + data.SetChooseFull(true); + data.SetChooseAlpha(false); + data.SetColour(wxColour(_selection.PrimaryColor("#26A69A"))); + + std::vector custom_colors; + if (wxGetApp().app_config != nullptr) + custom_colors = wxGetApp().app_config->get_custom_color_from_config(); + + const int custom_count = std::min(static_cast(custom_colors.size()), CUSTOM_COLOR_COUNT); + for (int index = 0; index < custom_count; ++index) + data.SetCustomColour(index, string_to_wxColor(custom_colors[index])); + + wxColourDialog dialog(this, &data); + dialog.SetTitle(_L("Please choose the filament color")); + if (dialog.ShowModal() != wxID_OK) + return; + + const wxColourData result = dialog.GetColourData(); + if (custom_colors.size() != CUSTOM_COLOR_COUNT) + custom_colors.resize(CUSTOM_COLOR_COUNT); + for (int index = 0; index < CUSTOM_COLOR_COUNT; ++index) + custom_colors[index] = color_to_string(result.GetCustomColour(index)); + if (wxGetApp().app_config != nullptr) + wxGetApp().app_config->save_custom_color_to_config(custom_colors); + + const wxColour color = result.GetColour(); + if (color.IsOk()) + SelectCustomColor(color.GetAsString(wxC2S_HTML_SYNTAX).ToStdString()); +} + +} // namespace GUI +} // namespace Slic3r diff --git a/src/slic3r/GUI/FilamentColorDialog.hpp b/src/slic3r/GUI/FilamentColorDialog.hpp new file mode 100644 index 00000000000..e74cb3ac355 --- /dev/null +++ b/src/slic3r/GUI/FilamentColorDialog.hpp @@ -0,0 +1,69 @@ +#pragma once + +#include "GUI_Utils.hpp" +#include "libslic3r/FilamentColorLibrary.hpp" + +#include +#include +#include + +class wxStaticBitmap; +class wxStaticText; + +namespace Slic3r +{ +namespace GUI +{ + +/** + * @brief Dialog for choosing a built-in filament color or a custom color. + */ +class FilamentColorDialog : public DPIDialog +{ +public: + /** + * @brief Creates the filament color dialog. + */ + FilamentColorDialog(wxWindow* parent, const FilamentColorInfo& filament, const FilamentColor& currentColor); + + /** + * @brief Gets the selected color result. + */ + const FilamentColor& Selection() const + { + return _selection; + } + +private: + void BuildUi(); + void SelectFilamentColor(const FilamentColorItem& colorItem); + void SelectCustomColor(const std::string& color); + void UpdatePreview(); + void UpdateSwatchSelection(); + void OpenMoreColorDialog(); + void PlaceNearFilamentPanel(); + void UpdateRoundedShape(); + void BindDragWindow(wxWindow* window); + void StartDrag(wxMouseEvent& event); + void DragDialog(wxMouseEvent& event); + void EndDrag(wxMouseEvent& event); + void on_dpi_changed(const wxRect& suggestedRect) override; + +private: + FilamentColorInfo _filament; + std::string _languageCode; + FilamentColor _selection; + std::string _selectedSku; + std::string _highlightSku; + std::vector> _swatchBySku; + wxStaticBitmap* _previewBitmap { nullptr }; + wxStaticText* _nameLabel { nullptr }; + wxStaticText* _skuLabel { nullptr }; + bool _dragPending { false }; + bool _isDragging { false }; + wxPoint _dragStartMouse; + wxPoint _dragStartPosition; +}; + +} // namespace GUI +} // namespace Slic3r diff --git a/src/slic3r/GUI/FilamentColorUtils.cpp b/src/slic3r/GUI/FilamentColorUtils.cpp new file mode 100644 index 00000000000..dc847da2717 --- /dev/null +++ b/src/slic3r/GUI/FilamentColorUtils.cpp @@ -0,0 +1,169 @@ +#include "FilamentColorUtils.hpp" + +#include "MixedFilamentBadge.hpp" +#include "libslic3r/PrintConfig.hpp" + +namespace Slic3r +{ +namespace GUI +{ +namespace FilamentColorUtils +{ +namespace +{ + +std::vector NormalizedColorsOrFallback(const std::vector& colors, + const std::string& fallback_color) +{ + std::vector normalized; + normalized.reserve(colors.size()); + for (const std::string& color : colors) + { + const std::string normalized_color = NormalizeHexColor(color); + if (!normalized_color.empty()) + normalized.emplace_back(normalized_color); + } + + if (normalized.empty()) + { + const std::string fallback = NormalizeHexColor(fallback_color, "#26A69A"); + normalized.emplace_back(fallback.empty() ? "#26A69A" : fallback); + } + + return normalized; +} + +void AddFallbackToNormalizedColors(std::vector& colors, const std::string& fallbackColor) +{ + if (!colors.empty()) + return; + + const std::string fallback = NormalizeHexColor(fallbackColor, "#26A69A"); + colors.emplace_back(fallback.empty() ? "#26A69A" : fallback); +} + +wxColour WxColorFromHex(const std::string& color) +{ + wxColour parsed(color); + return parsed.IsOk() ? parsed : wxColour("#26A69A"); +} + +wxBitmap* GetFilamentColorIconFromNormalized(const std::vector& normalizedColors, FilamentColorMode mode, + const std::string& label, int iconWidth, int iconHeight, + const wxColour& lightBorderColor) +{ + std::vector wxColors; + wxColors.reserve(normalizedColors.size()); + for (const std::string& color : normalizedColors) + wxColors.emplace_back(WxColorFromHex(color)); + + const bool isGradient = normalizedColors.size() > 1 && mode == FilamentColorMode::Gradient; + return get_color_block_bitmap_cached(wxColors, isGradient, iconWidth, iconHeight, wxString::FromUTF8(label.c_str()), + lightBorderColor); +} + +} // namespace + +std::string NormalizeHexColor(const std::string& color) +{ + return NormalizeFilamentHexColor(color); +} + +std::string NormalizeHexColor(const std::string& color, const std::string& fallback_color) +{ + return NormalizeFilamentHexColor(color, fallback_color); +} + +std::string StripHashForPreprint(const std::string& color) +{ + std::string normalized = NormalizeHexColor(color); + if (!normalized.empty() && normalized.front() == '#') + normalized.erase(normalized.begin()); + return normalized; +} + +std::string StripHashForPreprint(const std::string& color, const std::string& fallback_color) +{ + std::string normalized = NormalizeHexColor(color, fallback_color); + if (!normalized.empty() && normalized.front() == '#') + normalized.erase(normalized.begin()); + return normalized; +} + +std::vector SplitMultiColors(const std::string& value) +{ + return SplitFilamentMultiColors(value); +} + +std::string JoinMultiColors(const std::vector& colors) +{ + return JoinFilamentMultiColors(colors); +} + +std::string GetPrimaryColor(const std::vector& colors, const std::string& fallback_color) +{ + const FilamentColor color = FilamentColor::FromColors(colors, FilamentColorMode::Segment, fallback_color); + return color.PrimaryColor(fallback_color); +} + +std::string GetFilamentMatchName(const std::string& name) +{ + return Slic3r::GetFilamentMatchName(name); +} + +FilamentColor GetFilamentColorFromConfig(const DynamicPrintConfig* config, size_t colorIndex, const std::string& fallbackColor) +{ + std::vector colors; + FilamentColorMode mode = FilamentColorMode::Segment; + + if (config != nullptr && config->has("filament_multi_colors")) + { + const ConfigOptionStrings* option = config->option("filament_multi_colors"); + if (option != nullptr && option->values.size() > colorIndex) + colors = SplitMultiColors(option->values[colorIndex]); + } + + if (config != nullptr && config->has("filament_colour_mode")) + { + const ConfigOptionInts* option = config->option("filament_colour_mode"); + if (option != nullptr && option->values.size() > colorIndex) + mode = FilamentColorModeFromConfig(option->values[colorIndex]); + } + + return FilamentColor::FromColors(colors, mode, fallbackColor); +} + +nlohmann::json BuildPreprintColorMultiItem(const std::string& multiColors, FilamentColorMode mode, + const std::string& fallbackColor) +{ + const FilamentColor color = FilamentColor::FromMultiColors(multiColors, mode, fallbackColor); + + nlohmann::json out_colors = nlohmann::json::array(); + for (const std::string& item : color.colors) + out_colors.push_back(item.substr(1)); + + nlohmann::json item; + item["mode"] = FilamentColorModeToConfig(color.NormalizedMode()); + item["nums"] = color.colors.size(); + item["colors"] = out_colors; + return item; +} + +wxBitmap* GetFilamentColorIcon(const std::vector& colors, FilamentColorMode mode, const std::string& label, + int iconWidth, int iconHeight, const wxColour& lightBorderColor) +{ + const std::vector normalized = NormalizedColorsOrFallback(colors, "#26A69A"); + return GetFilamentColorIconFromNormalized(normalized, mode, label, iconWidth, iconHeight, lightBorderColor); +} + +wxBitmap* GetFilamentColorIcon(const std::string& multiColors, FilamentColorMode mode, const std::string& fallbackColor, + const std::string& label, int iconWidth, int iconHeight, const wxColour& lightBorderColor) +{ + std::vector normalized = SplitMultiColors(multiColors); + AddFallbackToNormalizedColors(normalized, fallbackColor); + return GetFilamentColorIconFromNormalized(normalized, mode, label, iconWidth, iconHeight, lightBorderColor); +} + +} // namespace FilamentColorUtils +} // namespace GUI +} // namespace Slic3r diff --git a/src/slic3r/GUI/FilamentColorUtils.hpp b/src/slic3r/GUI/FilamentColorUtils.hpp new file mode 100644 index 00000000000..eff0210745a --- /dev/null +++ b/src/slic3r/GUI/FilamentColorUtils.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include "libslic3r/FilamentColorLibrary.hpp" + +#include +#include +#include + +#include +#include +#include + +namespace Slic3r +{ + +class DynamicPrintConfig; + +namespace GUI +{ +namespace FilamentColorUtils +{ + +std::string NormalizeHexColor(const std::string& color); +std::string NormalizeHexColor(const std::string& color, const std::string& fallback_color); + +std::string StripHashForPreprint(const std::string& color); +std::string StripHashForPreprint(const std::string& color, const std::string& fallback_color); + +std::vector SplitMultiColors(const std::string& value); +std::string JoinMultiColors(const std::vector& colors); + +std::string GetPrimaryColor(const std::vector& colors, const std::string& fallback_color); + +std::string GetFilamentMatchName(const std::string& name); + +FilamentColor GetFilamentColorFromConfig(const DynamicPrintConfig* config, size_t colorIndex, const std::string& fallbackColor); + +nlohmann::json BuildPreprintColorMultiItem(const std::string& multiColors, FilamentColorMode mode, + const std::string& fallbackColor); + +wxBitmap* GetFilamentColorIcon(const std::vector& colors, FilamentColorMode mode, const std::string& label, + int iconWidth, int iconHeight, const wxColour& lightBorderColor = wxNullColour); + +wxBitmap* GetFilamentColorIcon(const std::string& multiColors, FilamentColorMode mode, const std::string& fallbackColor, + const std::string& label, int iconWidth, int iconHeight, + const wxColour& lightBorderColor = wxNullColour); + +} // namespace FilamentColorUtils +} // namespace GUI +} // namespace Slic3r diff --git a/src/slic3r/GUI/MixedFilamentBadge.cpp b/src/slic3r/GUI/MixedFilamentBadge.cpp index 8981a7616a3..b522400c72e 100644 --- a/src/slic3r/GUI/MixedFilamentBadge.cpp +++ b/src/slic3r/GUI/MixedFilamentBadge.cpp @@ -233,6 +233,142 @@ wxBitmap* get_color_block_bitmap_cached(const ColorBlockParams& params) return cache.insert(key, bmp); } +wxBitmap* get_color_block_bitmap_cached(const std::vector& colors, bool is_gradient, + int width, int height, const wxString& label, + const wxColour& lightBorderColor) +{ + wxASSERT(wxIsMainThread()); + static BitmapCache cache; + + width = std::max(1, width); + height = std::max(1, height); + + std::vector drawColors; + drawColors.reserve(colors.empty() ? 1 : colors.size()); + if (colors.empty()) + { + drawColors.emplace_back(wxColour("#26A69A")); + } + else + { + for (const wxColour& color : colors) + drawColors.emplace_back(color.IsOk() ? color : wxColour("#26A69A")); + } + + const bool useGradient = is_gradient && drawColors.size() > 1; + std::string key = useGradient ? "official-grad:" : "official-seg:"; + key += "h" + std::to_string(height) + ":w" + std::to_string(width) + ":" + label.ToStdString(); + for (const wxColour& color : drawColors) + { + key += ":"; + key += color.GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); + } + if (lightBorderColor.IsOk()) + { + key += ":border:"; + key += lightBorderColor.GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); + } + + wxBitmap* cached = cache.find(key); + if (cached != nullptr) + return cached; + + wxBitmap bmp(width, height); + wxMemoryDC dc; + dc.SelectObject(bmp); + dc.SetPen(*wxTRANSPARENT_PEN); + const bool useSmallFont = std::min(width, height) < 20; + dc.SetFont(useSmallFont ? ::Label::Body_8 : ::Label::Body_12); + + if (drawColors.size() <= 1) + { + dc.SetBackground(wxBrush(drawColors.front())); + dc.Clear(); + dc.SetBrush(wxBrush(drawColors.front())); + dc.DrawRectangle(0, 0, width, height); + } + else if (useGradient) + { + dc.SetBrush(wxBrush(drawColors.front())); + dc.DrawRectangle(0, 0, width, height); + const int segmentCount = static_cast(drawColors.size()) - 1; + int left = 0; + for (int index = 0; index < segmentCount; ++index) + { + const int right = index == segmentCount - 1 ? width : width * (index + 1) / segmentCount; + const int segmentWidth = right - left; + if (segmentWidth > 0) + dc.GradientFillLinear(wxRect(left, 0, segmentWidth, height), drawColors[static_cast(index)], + drawColors[static_cast(index + 1)], wxEAST); + left = right; + } + } + else + { + const int colorCount = static_cast(drawColors.size()); + int left = 0; + for (int index = 0; index < colorCount; ++index) + { + const int right = index == colorCount - 1 ? width : width * (index + 1) / colorCount; + const int segmentWidth = right - left; + dc.SetBrush(wxBrush(drawColors[static_cast(index)])); + if (segmentWidth > 0) + dc.DrawRectangle(left, 0, segmentWidth, height); + left = right; + } + } + + auto colorIsVeryLight = [](const wxColour& color) -> bool + { + return color.Red() > 224 && color.Green() > 224 && color.Blue() > 224; + }; + + bool veryLight = true; + if (!useGradient && drawColors.size() > 1) + { + veryLight = false; + for (const wxColour& color : drawColors) + { + if (colorIsVeryLight(color)) + { + veryLight = true; + break; + } + } + } + else + { + for (const wxColour& color : drawColors) + { + if (!colorIsVeryLight(color)) + { + veryLight = false; + break; + } + } + } + + if (veryLight) + { + dc.SetPen(lightBorderColor.IsOk() ? wxPen(lightBorderColor, 1) : *wxGREY_PEN); + dc.SetBrush(*wxTRANSPARENT_BRUSH); + dc.DrawRectangle(0, 0, width, height); + } + + if (!label.empty()) + { + double averageLuminance = 0.0; + for (const wxColour& color : drawColors) + averageLuminance += color.GetLuminance(); + averageLuminance /= static_cast(drawColors.size()); + dc.SetTextForeground(averageLuminance < 0.51 ? *wxWHITE : *wxBLACK); + dc.DrawLabel(label, wxRect(0, 0, width, height), wxALIGN_CENTER_HORIZONTAL | wxALIGN_CENTER_VERTICAL); + } + + dc.SelectObject(wxNullBitmap); + return cache.insert(key, bmp); +} + // --------------------------------------------------------------------------- // Free function — shared between badge and merge menus // --------------------------------------------------------------------------- diff --git a/src/slic3r/GUI/MixedFilamentBadge.hpp b/src/slic3r/GUI/MixedFilamentBadge.hpp index 2058482c2df..ba043794bd4 100644 --- a/src/slic3r/GUI/MixedFilamentBadge.hpp +++ b/src/slic3r/GUI/MixedFilamentBadge.hpp @@ -33,6 +33,11 @@ wxColour interpolate_color(const std::vector& colors, double pos); // Key format: "solid:#RRGGBB:hH:wW:label" or "grad:#RRGGBB:#RRGGBBBT:hH:wW:label" wxBitmap* get_color_block_bitmap_cached(const ColorBlockParams& params); +// Cached bitmap for official filament colour blocks. Multiple colours are drawn left to right. +wxBitmap* get_color_block_bitmap_cached(const std::vector& colors, bool is_gradient, + int width, int height, const wxString& label, + const wxColour& lightBorderColor); + class MixedFilamentBadge : public wxPanel { public: diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 3afd3ec6412..45c5ecb5eae 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -10030,6 +10030,8 @@ std::vector Plater::priv::load_files(const std::vector& input_ if (current_project_empty) { static const t_config_option_keys imported_project_option_keys = { "filament_colour", + "filament_multi_colors", + "filament_colour_mode", "mixed_filament_definitions", "mixed_filament_gradient_mode", "mixed_filament_height_lower_bound", diff --git a/src/slic3r/GUI/PresetComboBoxes.cpp b/src/slic3r/GUI/PresetComboBoxes.cpp index 06fd0cb0131..08a29271f03 100644 --- a/src/slic3r/GUI/PresetComboBoxes.cpp +++ b/src/slic3r/GUI/PresetComboBoxes.cpp @@ -1,6 +1,8 @@ #include "PresetComboBoxes.hpp" #include +#include +#include #include #include #include @@ -29,7 +31,9 @@ #include "GUI.hpp" #include "GUI_App.hpp" +#include "FilamentColorUtils.hpp" #include "NotificationManager.hpp" +#include "FilamentColorDialog.hpp" #include "Plater.hpp" #include "MainFrame.hpp" #include "format.hpp" @@ -58,6 +62,97 @@ namespace GUI { #define BORDER_W 10 +namespace +{ + +std::string ConfigStringAt(const DynamicPrintConfig& config, const std::string& key, int index) +{ + if (index < 0 || !config.has(key)) + return {}; + + const ConfigOptionStrings* option = config.option(key); + const size_t value_index = static_cast(index); + return option != nullptr && option->values.size() > value_index ? option->values[value_index] : std::string(); +} + +int ConfigIntAt(const DynamicPrintConfig& config, const std::string& key, int index) +{ + if (index < 0 || !config.has(key)) + return 0; + + const ConfigOptionInts* option = config.option(key); + const size_t value_index = static_cast(index); + return option != nullptr && option->values.size() > value_index ? option->values[value_index] : 0; +} + +ConfigOptionStrings* CloneStringOption(const DynamicPrintConfig& config, const std::string& key) +{ + const ConfigOptionStrings* option = config.option(key); + return option != nullptr ? static_cast(option->clone()) : new ConfigOptionStrings{}; +} + +ConfigOptionInts* CloneIntOption(const DynamicPrintConfig& config, const std::string& key) +{ + const ConfigOptionInts* option = config.option(key); + return option != nullptr ? static_cast(option->clone()) : new ConfigOptionInts{}; +} + +void ResizeStrings(ConfigOptionStrings* option, size_t size) +{ + if (option != nullptr && option->values.size() < size) + option->values.resize(size); +} + +void ResizeInts(ConfigOptionInts* option, size_t size) +{ + if (option != nullptr && option->values.size() < size) + option->values.resize(size); +} + +std::string FilamentBaseName(std::string name) +{ + name = Preset::remove_suffix_modified(name); + return FilamentColorUtils::GetFilamentMatchName(name); +} + +bool IsSnapmakerFilamentName(const std::string& name) +{ + return boost::algorithm::istarts_with(name, "Snapmaker"); +} + +bool FilamentMatchesPresetName(const FilamentColorInfo& filament, const std::string& presetName) +{ + const std::string filamentName = FilamentBaseName(filament.filamentName); + const std::string currentFilamentName = FilamentBaseName(presetName); + return !filamentName.empty() && filamentName == currentFilamentName; +} + +wxSize FilamentColorPickerBitmapSize(const wxButton* picker) +{ + wxSize size = picker != nullptr ? picker->GetSize() : wxSize(); + if (size.GetWidth() <= 0 || size.GetHeight() <= 0) + size = picker != nullptr ? picker->GetClientSize() : wxSize(); + if (size.GetWidth() <= 0 || size.GetHeight() <= 0) + size = wxSize(20, 20); + + return size; +} + +wxBitmap* GetFilamentColorPickerBitmap(const DynamicPrintConfig& config, + int filamentIdx, + const std::string& fallbackColor, + const wxSize& size) +{ + const std::string multiColors = ConfigStringAt(config, "filament_multi_colors", filamentIdx); + const int colorModeValue = ConfigIntAt(config, "filament_colour_mode", filamentIdx); + const FilamentColorMode colorMode = FilamentColorModeFromConfig(colorModeValue); + const std::string label = filamentIdx >= 0 ? std::to_string(filamentIdx + 1) : std::string(); + return FilamentColorUtils::GetFilamentColorIcon(multiColors, colorMode, fallbackColor, label, + std::max(1, size.GetWidth()), std::max(1, size.GetHeight())); +} + +} // namespace + // --------------------------------- // *** PresetComboBox *** // --------------------------------- @@ -233,10 +328,23 @@ int PresetComboBox::update_ams_color() int idx = selected_ams_filament(); auto& filaments = wxGetApp().preset_bundle->machine_filaments; + const ConnectMachineInfo* machineInfo = nullptr; + if (idx >= 0) + { + const std::vector& machineInfoList = wxGetApp().preset_bundle->m_connect_machine_info_list; + const size_t machineInfoIndex = static_cast(idx); + if (machineInfoIndex < machineInfoList.size()) + machineInfo = &machineInfoList[machineInfoIndex]; + } int real_idx = -1; - int tmp = idx; - if (tmp >= 0) { + if (machineInfo != nullptr) + { + real_idx = machineInfo->index; + } + else if (idx >= 0) + { + int tmp = idx; for (auto iter = filaments.begin(); iter != filaments.end(); ++iter) { if (tmp == 0) { real_idx = iter->first; @@ -258,35 +366,81 @@ int PresetComboBox::update_ams_color() } std::string color; - if (idx < 0) { + std::vector multiColors; + FilamentColorMode colorMode = FilamentColorMode::Segment; + if (idx < 0) + { auto *preset = m_collection->find_preset(Preset::remove_suffix_modified(GetLabel().ToUTF8().data())); if (preset) color = preset->config.opt_string("default_filament_colour", 0u); if (color.empty()) return -1; - } else { - if (wxGetApp().preset_bundle->machine_filaments.size() > 0) { + } + else if (machineInfo != nullptr) + { + color = machineInfo->color_info; + multiColors = machineInfo->multiColors; + colorMode = machineInfo->colorMode; + if (color.empty() && !multiColors.empty()) + color = multiColors.front(); + } + else + { + if (wxGetApp().preset_bundle->machine_filaments.size() > 0) + { auto iter = wxGetApp().preset_bundle->machine_filaments.begin(); for (size_t i = 0; iter != wxGetApp().preset_bundle->machine_filaments.end() && i < idx; ++i) { ++iter; } - if (iter == wxGetApp().preset_bundle->machine_filaments.end()) { + if (iter == wxGetApp().preset_bundle->machine_filaments.end()) + { return -1; } color = iter->second.second; - } else { + } + else + { auto& ams_list = wxGetApp().preset_bundle->filament_ams_list; auto iter = ams_list.find(idx); - if (iter == ams_list.end()) { + if (iter == ams_list.end()) + { BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": ams %1% out of range %2%") % idx % ams_list.size(); return -1; } color = iter->second.opt_string("filament_colour", 0u); } } - DynamicPrintConfig *cfg = &wxGetApp().preset_bundle->project_config; - auto colors = static_cast(cfg->option("filament_colour")->clone()); - colors->values[m_filament_idx] = color; + const std::string normalizedColor = FilamentColorUtils::NormalizeHexColor(color, "#26A69A"); + if (machineInfo != nullptr && multiColors.empty() && !normalizedColor.empty()) + multiColors.emplace_back(normalizedColor); + int normalizedMode = FilamentColorModeToConfig(FilamentColorMode::Segment); + std::string joinedMultiColors; + if (machineInfo != nullptr) + { + joinedMultiColors = FilamentColorUtils::JoinMultiColors(multiColors); + if (multiColors.size() > 1) + normalizedMode = FilamentColorModeToConfig(colorMode); + } + + DynamicPrintConfig* cfg = &wxGetApp().preset_bundle->project_config; + const size_t targetSize = static_cast(m_filament_idx) + 1; + const size_t index = static_cast(m_filament_idx); + ConfigOptionStrings* filamentColors = CloneStringOption(*cfg, "filament_colour"); + ResizeStrings(filamentColors, targetSize); + + filamentColors->values[index] = normalizedColor.empty() ? color : normalizedColor; DynamicPrintConfig new_cfg; - new_cfg.set_key_value("filament_colour", colors); + new_cfg.set_key_value("filament_colour", filamentColors); + if (machineInfo != nullptr) + { + ConfigOptionStrings* filamentMultiColors = CloneStringOption(*cfg, "filament_multi_colors"); + ConfigOptionInts* filamentColourModes = CloneIntOption(*cfg, "filament_colour_mode"); + ResizeStrings(filamentMultiColors, targetSize); + ResizeInts(filamentColourModes, targetSize); + + filamentMultiColors->values[index] = joinedMultiColors; + filamentColourModes->values[index] = normalizedMode; + new_cfg.set_key_value("filament_multi_colors", filamentMultiColors); + new_cfg.set_key_value("filament_colour_mode", filamentColourModes); + } cfg->apply(new_cfg); wxGetApp().plater()->on_config_change(new_cfg); //trigger the filament color changed @@ -745,66 +899,18 @@ PlaterPresetComboBox::PlaterPresetComboBox(wxWindow *parent, Preset::Type preset // BBS // Swallow the mouse click and open the color picker. - //change_extruder_color(); + //ChangeExtruderColor(); }); #endif } // BBS if (m_type == Preset::TYPE_FILAMENT) { - int em = wxGetApp().em_unit(); clr_picker = new wxBitmapButton(parent, wxID_ANY, {}, wxDefaultPosition, wxSize(FromDIP(20), FromDIP(20)), wxBU_EXACTFIT | wxBU_AUTODRAW | wxBORDER_NONE); + clr_picker->SetBitmapMargins(0, 0); clr_picker->SetToolTip(_L("Click to select filament color")); - clr_picker->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e) { - m_clrData.SetColour(clr_picker->GetBackgroundColour()); - m_clrData.SetChooseFull(true); - m_clrData.SetChooseAlpha(false); - - std::vector colors = wxGetApp().app_config->get_custom_color_from_config(); - for (int i = 0; i < colors.size(); i++) { - m_clrData.SetCustomColour(i, string_to_wxColor(colors[i])); - } - wxColourDialog dialog(this, &m_clrData); - dialog.SetTitle(_L("Please choose the filament color")); - if ( dialog.ShowModal() == wxID_OK ) - { - m_clrData = dialog.GetColourData(); - if (colors.size() != CUSTOM_COLOR_COUNT) { - colors.resize(CUSTOM_COLOR_COUNT); - } - for (int i = 0; i < CUSTOM_COLOR_COUNT; i++) { - colors[i] = color_to_string(m_clrData.GetCustomColour(i)); - } - wxGetApp().app_config->save_custom_color_to_config(colors); - // get current color - DynamicPrintConfig* cfg = &wxGetApp().preset_bundle->project_config; - auto colors = static_cast(cfg->option("filament_colour")->clone()); - - // clear filament_extruder_map - auto& filament_extruder_map = wxGetApp().app_config->get_filament_extruder_map_ref(); - if (filament_extruder_map.count(m_filament_idx)) { - filament_extruder_map.erase(m_filament_idx); - } - - wxColour clr(colors->values[m_filament_idx]); - if (!clr.IsOk()) - clr = wxColour(0, 0, 0); // Don't set alfa to transparence - - colors->values[m_filament_idx] = m_clrData.GetColour().GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); - DynamicPrintConfig cfg_new = *cfg; - cfg_new.set_key_value("filament_colour", colors); - - //wxGetApp().get_tab(Preset::TYPE_PRINTER)->load_config(cfg_new); - cfg->apply(cfg_new); - wxGetApp().plater()->update_project_dirty_from_presets(); - wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); - update(); - wxGetApp().plater()->on_config_change(cfg_new); - - wxCommandEvent *evt = new wxCommandEvent(EVT_FILAMENT_COLOR_CHANGED); - evt->SetInt(m_filament_idx); - wxQueueEvent(wxGetApp().plater(), evt); - } + clr_picker->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { + ChangeExtruderColor(); }); } else { @@ -936,32 +1042,171 @@ bool PlaterPresetComboBox::switch_to_tab() return true; } -void PlaterPresetComboBox::change_extruder_color() +void PlaterPresetComboBox::ChangeExtruderColor() { - // get current color - DynamicPrintConfig* cfg = &wxGetApp().preset_bundle->project_config; - auto colors = static_cast(cfg->option("filament_colour")->clone()); - wxColour clr(colors->values[m_filament_idx]); - if (!clr.IsOk()) - clr = wxColour(0, 0, 0); // Don't set alfa to transparence - - auto data = new wxColourData(); - data->SetChooseFull(1); - data->SetColour(clr); - - wxColourDialog dialog(this, data); - dialog.CenterOnParent(); - if (dialog.ShowModal() == wxID_OK) + if (m_filament_idx < 0 || wxGetApp().preset_bundle == nullptr) + return; + + const std::string filamentPresetName = CurrentFilamentPresetName(); + const std::string filamentBaseName = FilamentBaseName(filamentPresetName); + if (!IsSnapmakerFilamentName(filamentBaseName)) { - colors->values[m_filament_idx] = dialog.GetColourData().GetColour().GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); + SelectLegacyFilamentColor(); + return; + } - DynamicPrintConfig cfg_new = *cfg; - cfg_new.set_key_value("filament_colour", colors); + DynamicPrintConfig& config = wxGetApp().preset_bundle->project_config; + const std::string currentColorHex = ConfigStringAt(config, "filament_colour", m_filament_idx); + const std::string currentMultiColors = ConfigStringAt(config, "filament_multi_colors", m_filament_idx); + const int currentModeValue = ConfigIntAt(config, "filament_colour_mode", m_filament_idx); - wxGetApp().get_tab(Preset::TYPE_PRINTER)->load_config(cfg_new); - this->update(); - wxGetApp().plater()->on_config_change(cfg_new); + FilamentColorLibrary& library = FilamentColorLibrary::Instance(); + FilamentColorInfo filament; + const std::string filamentId = CurrentFilamentId(); + bool foundFilament = false; + if (library.EnsureLoaded()) + { + foundFilament = !filamentPresetName.empty() && library.FindFilamentByName(filamentPresetName, filament); + if (!foundFilament) + foundFilament = !filamentId.empty() && library.FindFilamentById(filamentId, filament) && + FilamentMatchesPresetName(filament, filamentPresetName); + } + + if (foundFilament && !filament.colors.empty()) + { + const FilamentColorMode currentMode = FilamentColorModeFromConfig(currentModeValue); + const FilamentColor currentColor = FilamentColor::FromMultiColors(currentMultiColors, currentMode, currentColorHex); + FilamentColorDialog dialog(this, filament, currentColor); + + if (dialog.ShowModal() == wxID_OK) + { + ApplyFilamentColor(dialog.Selection()); + } + return; + } + + SelectLegacyFilamentColor(); +} + +void PlaterPresetComboBox::SelectLegacyFilamentColor() +{ + if (m_filament_idx < 0 || wxGetApp().preset_bundle == nullptr) + return; + + DynamicPrintConfig& config = wxGetApp().preset_bundle->project_config; + wxColour current_color(ConfigStringAt(config, "filament_colour", m_filament_idx)); + if (!current_color.IsOk()) + current_color = wxColour(0, 0, 0); + + m_clrData.SetColour(current_color); + m_clrData.SetChooseFull(true); + m_clrData.SetChooseAlpha(false); + + std::vector custom_colors; + if (wxGetApp().app_config != nullptr) + custom_colors = wxGetApp().app_config->get_custom_color_from_config(); + + const int custom_count = std::min(static_cast(custom_colors.size()), CUSTOM_COLOR_COUNT); + for (int i = 0; i < custom_count; ++i) + m_clrData.SetCustomColour(i, string_to_wxColor(custom_colors[i])); + + wxColourDialog dialog(this, &m_clrData); + dialog.SetTitle(_L("Please choose the filament color")); + if (dialog.ShowModal() != wxID_OK) + return; + + m_clrData = dialog.GetColourData(); + if (custom_colors.size() != CUSTOM_COLOR_COUNT) + custom_colors.resize(CUSTOM_COLOR_COUNT); + for (int i = 0; i < CUSTOM_COLOR_COUNT; ++i) + custom_colors[i] = color_to_string(m_clrData.GetCustomColour(i)); + if (wxGetApp().app_config != nullptr) + wxGetApp().app_config->save_custom_color_to_config(custom_colors); + + const std::string selected_color = m_clrData.GetColour().GetAsString(wxC2S_HTML_SYNTAX).ToStdString(); + ApplyFilamentColor(FilamentColor::FromMultiColors("", FilamentColorMode::Segment, selected_color)); +} + +void PlaterPresetComboBox::ApplyFilamentColor(const FilamentColor& colorData) +{ + if (m_filament_idx < 0 || wxGetApp().preset_bundle == nullptr) + return; + + DynamicPrintConfig* config = &wxGetApp().preset_bundle->project_config; + const size_t index = static_cast(m_filament_idx); + const size_t target_size = index + 1; + + const FilamentColor normalizedColor = FilamentColor::FromColors(colorData.colors, colorData.mode); + const std::string normalizedPrimary = normalizedColor.PrimaryColor("#26A69A"); + const std::string multiColors = normalizedColor.ToMultiColorsString(); + const int normalizedMode = FilamentColorModeToConfig(normalizedColor.NormalizedMode()); + + ConfigOptionStrings* filament_colors = CloneStringOption(*config, "filament_colour"); + ConfigOptionStrings* filament_multi_colors = CloneStringOption(*config, "filament_multi_colors"); + ConfigOptionInts* filament_colour_modes = CloneIntOption(*config, "filament_colour_mode"); + + ResizeStrings(filament_colors, target_size); + ResizeStrings(filament_multi_colors, target_size); + ResizeInts(filament_colour_modes, target_size); + + filament_colors->values[index] = normalizedPrimary; + filament_multi_colors->values[index] = multiColors; + filament_colour_modes->values[index] = normalizedMode; + + if (wxGetApp().app_config != nullptr) + { + std::unordered_map& filament_extruder_map = wxGetApp().app_config->get_filament_extruder_map_ref(); + if (filament_extruder_map.count(m_filament_idx)) + filament_extruder_map.erase(m_filament_idx); } + + DynamicPrintConfig new_config = *config; + new_config.set_key_value("filament_colour", filament_colors); + new_config.set_key_value("filament_multi_colors", filament_multi_colors); + new_config.set_key_value("filament_colour_mode", filament_colour_modes); + + config->apply(new_config); + wxGetApp().plater()->update_project_dirty_from_presets(); + if (wxGetApp().app_config != nullptr) + wxGetApp().preset_bundle->export_selections(*wxGetApp().app_config); + update(); + wxGetApp().plater()->on_config_change(new_config); + + wxCommandEvent* event = new wxCommandEvent(EVT_FILAMENT_COLOR_CHANGED); + event->SetInt(m_filament_idx); + wxQueueEvent(wxGetApp().plater(), event); +} + +std::string PlaterPresetComboBox::CurrentFilamentPresetName() const +{ + std::string presetName; + if (m_preset_bundle != nullptr && m_filament_idx >= 0) + { + const size_t index = static_cast(m_filament_idx); + if (m_preset_bundle->filament_presets.size() > index) + presetName = Preset::remove_suffix_modified(m_preset_bundle->filament_presets[index]); + } + + if (presetName.empty() && GetSelection() != wxNOT_FOUND) + presetName = Preset::remove_suffix_modified(GetString(GetSelection()).ToUTF8().data()); + + if (!presetName.empty() && m_collection != nullptr) + presetName = Preset::remove_suffix_modified(m_collection->get_preset_name_by_alias(presetName)); + + return presetName; +} + +std::string PlaterPresetComboBox::CurrentFilamentId() const +{ + if (m_preset_bundle == nullptr || m_collection == nullptr || m_filament_idx < 0) + return {}; + + const std::string presetName = CurrentFilamentPresetName(); + if (presetName.empty()) + return {}; + + const Preset* preset = m_collection->find_preset(presetName, false, true); + return preset != nullptr ? preset->filament_id : std::string(); } void PlaterPresetComboBox::show_add_menu() @@ -987,7 +1232,7 @@ void PlaterPresetComboBox::show_edit_menu() // To edit extruder color from the sidebar if (m_type == Preset::TYPE_FILAMENT) { append_menu_item(menu, wxID_ANY, _devL("Change extruder color"), "", - [this](wxCommandEvent&) { this->change_extruder_color(); }, "blank_14", menu, []() { return true; }, wxGetApp().plater()); + [this](wxCommandEvent&) { this->ChangeExtruderColor(); }, "blank_14", menu, []() { return true; }, wxGetApp().plater()); wxGetApp().plater()->PopupMenu(menu); return; } @@ -1027,8 +1272,14 @@ void PlaterPresetComboBox::update() //unsigned char rgb[3]; filament_color = m_preset_bundle->project_config.opt_string("filament_colour", (unsigned int) m_filament_idx); wxColor clr(filament_color); + const int picker_size = std::max(1, 20 * m_em_unit / 10); + clr_picker->SetMinSize(wxSize(picker_size, picker_size)); + clr_picker->SetSize(picker_size, picker_size); clr_picker->SetBackgroundColour(clr); - clr_picker->SetBitmap(*get_extruder_color_icons(true)[m_filament_idx]); + wxBitmap* color_bitmap = GetFilamentColorPickerBitmap(m_preset_bundle->project_config, m_filament_idx, + filament_color, FilamentColorPickerBitmapSize(clr_picker)); + if (color_bitmap != nullptr) + clr_picker->SetBitmap(*color_bitmap); #ifdef __WXOSX__ clr_picker->SetLabel(clr_picker->GetLabel()); // Let setBezelStyle: be called clr_picker->Refresh(); @@ -1189,9 +1440,15 @@ void PlaterPresetComboBox::update() if (item_iter != filaments.end()) { const_cast(*item_iter).is_visible = true; - auto color = machine_nozzles_list[i].color_info; - auto name = std::to_string(i + 1); - wxBitmap bmp(*get_extruder_color_icon(color, name, 24, 16)); + const ConnectMachineInfo& machineInfo = machine_nozzles_list[i]; + std::vector colors = machineInfo.multiColors; + if (colors.empty() && !machineInfo.color_info.empty()) + colors.emplace_back(machineInfo.color_info); + const std::string name = std::to_string(i + 1); + wxBitmap* icon = FilamentColorUtils::GetFilamentColorIcon(colors, machineInfo.colorMode, name, 24, 16); + if (icon == nullptr) + icon = get_extruder_color_icon(machineInfo.color_info, name, 24, 16); + wxBitmap bmp(*icon); Append(get_preset_name(*item_iter), bmp.ConvertToImage(), &m_first_ams_filament + i); } } @@ -1754,9 +2011,15 @@ void TabPresetComboBox::update() if (item_iter != filaments.end()) { const_cast(*item_iter).is_visible = true; - auto color = machine_nozzles_list[i].color_info; - auto name = std::to_string(i + 1); - wxBitmap bmp(*get_extruder_color_icon(color, name, 24, 16)); + const ConnectMachineInfo& machineInfo = machine_nozzles_list[i]; + std::vector colors = machineInfo.multiColors; + if (colors.empty() && !machineInfo.color_info.empty()) + colors.emplace_back(machineInfo.color_info); + const std::string name = std::to_string(i + 1); + wxBitmap* icon = FilamentColorUtils::GetFilamentColorIcon(colors, machineInfo.colorMode, name, 24, 16); + if (icon == nullptr) + icon = get_extruder_color_icon(machineInfo.color_info, name, 24, 16); + wxBitmap bmp(*icon); Append(get_preset_name(*item_iter), bmp.ConvertToImage(), &m_first_ams_filament + i); } } diff --git a/src/slic3r/GUI/PresetComboBoxes.hpp b/src/slic3r/GUI/PresetComboBoxes.hpp index 8d9d8cb1833..e557cc3ff6a 100644 --- a/src/slic3r/GUI/PresetComboBoxes.hpp +++ b/src/slic3r/GUI/PresetComboBoxes.hpp @@ -6,6 +6,7 @@ #include #include +#include "libslic3r/FilamentColorLibrary.hpp" #include "libslic3r/Preset.hpp" #include "wxExtensions.hpp" #include "BitmapComboBox.hpp" @@ -176,7 +177,7 @@ class PlaterPresetComboBox : public PresetComboBox wxColor get_color() { return m_color; } bool switch_to_tab(); - void change_extruder_color(); + void ChangeExtruderColor(); void show_add_menu(); void show_edit_menu(); @@ -250,6 +251,11 @@ class PlaterPresetComboBox : public PresetComboBox wxRect get_machine_connecting_btn_rect() const; wxRect get_edit_btn_rect() const; wxRect get_dropdown_rect() const; + + void SelectLegacyFilamentColor(); + void ApplyFilamentColor(const FilamentColor& colorData); + std::string CurrentFilamentPresetName() const; + std::string CurrentFilamentId() const; }; diff --git a/src/slic3r/GUI/SSWCP.cpp b/src/slic3r/GUI/SSWCP.cpp index ae6a482cef9..037a2ffeb34 100644 --- a/src/slic3r/GUI/SSWCP.cpp +++ b/src/slic3r/GUI/SSWCP.cpp @@ -1,5 +1,6 @@ // Implementation of web communication protocol for Slicer Studio #include "SSWCP.hpp" +#include "FilamentColorUtils.hpp" #include "GUI_App.hpp" #include "MainFrame.hpp" #include "DownloadManager.hpp" @@ -15,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -51,7 +53,6 @@ using namespace nlohmann; namespace Slic3r { namespace GUI { - // WCP_Logger WCP_Logger::WCP_Logger() { @@ -1643,14 +1644,41 @@ void SSWCP_Instance::update_filament_info(const json& objects, bool send_message } int extruder = j_value["extruder_map_table"][i].get(); + machineData.index = static_cast(i); + machineData.filament_info = name; + + json::const_iterator multiColorIt = j_value.find("filament_color_multi"); + if (multiColorIt != j_value.end() && multiColorIt->is_array() && multiColorIt->size() > i && + (*multiColorIt)[i].is_object()) + { + const json& multiColor = (*multiColorIt)[i]; + json::const_iterator colorsIt = multiColor.find("colors"); + if (colorsIt != multiColor.end() && colorsIt->is_array()) + { + for (const json& colorJson : *colorsIt) + { + if (!colorJson.is_string()) + continue; + + const std::string colorText = colorJson.get(); + const std::string normalized = FilamentColorUtils::NormalizeHexColor(colorText); + if (!normalized.empty()) + machineData.multiColors.emplace_back(normalized); + } + } + + json::const_iterator modeIt = multiColor.find("mode"); + if (machineData.multiColors.size() > 1 && modeIt != multiColor.end() && modeIt->is_number_integer()) + machineData.colorMode = FilamentColorModeFromConfig(modeIt->get()); + } if (j_value.count("filament_color_rgba") && j_value["filament_color_rgba"].is_array() && j_value["filament_color_rgba"].size() != 0) { std::string str_color = "#" + j_value["filament_color_rgba"][i].get(); - filaments.insert({int(i), {name, str_color}}); - machineData.index = i; + const std::string normalizedColor = FilamentColorUtils::NormalizeHexColor(str_color, "#FFFFFF"); + str_color = normalizedColor.empty() ? str_color : normalizedColor; + filaments.insert({int(i), {name, str_color}}); machineData.color_info = str_color; - machineData.filament_info = name; } else { if (j_value["filament_color"][i].is_number()) { int color = j_value["filament_color"][i].get(); @@ -1660,17 +1688,19 @@ void SSWCP_Instance::update_filament_info(const json& objects, bool send_message std::string str_color = oss.str(); filaments.insert({int(i), {name, str_color}}); - machineData.index = i; machineData.color_info = str_color; - machineData.filament_info = name; } else { std::string str_color = "#" + j_value["filament_color"][i].get(); + const std::string normalizedColor = FilamentColorUtils::NormalizeHexColor(str_color, "#FFFFFF"); + str_color = normalizedColor.empty() ? str_color : normalizedColor; filaments.insert({int(i), {name, str_color}}); - machineData.index = i; machineData.color_info = str_color; - machineData.filament_info = name; } } + if (machineData.multiColors.empty() && !machineData.color_info.empty()) + machineData.multiColors.emplace_back(machineData.color_info); + if (machineData.multiColors.size() <= 1) + machineData.colorMode = FilamentColorMode::Segment; if (j_value["nozzle_diameters"].is_array() && !j_value["nozzle_diameters"].empty()) machineData.nozzle_info = j_value["nozzle_diameters"][i].get(); machine_nozzles.push_back(machineData); @@ -3251,17 +3281,35 @@ void SSWCP_MachineOption_Instance::sw_GetFileFilamentMapping() // filament colour if (config.has("filament_colour")) { - auto filament_color = config.option("filament_colour")->values; + std::vector filament_color = config.option("filament_colour")->values; + const ConfigOptionStrings* filament_multi_colors = nullptr; + if (config.has("filament_multi_colors")) + filament_multi_colors = config.option("filament_multi_colors"); + + const ConfigOptionInts* filament_colour_modes = nullptr; + if (config.has("filament_colour_mode")) + filament_colour_modes = config.option("filament_colour_mode"); std::vector number_res(filament_color.size(), 0); std::vector str_res(filament_color.size()); - for (int i = 0; i < filament_color.size(); ++i) { + json multi_color_res = json::array(); + for (size_t i = 0; i < filament_color.size(); ++i) { number_res[i] = color_to_int(filament_color[i]); - str_res[i] = filament_color[i]; + str_res[i] = filament_color[i]; + + const bool has_multi_colors = filament_multi_colors != nullptr && filament_multi_colors->values.size() > i; + const bool has_mode = filament_colour_modes != nullptr && filament_colour_modes->values.size() > i; + const std::string multi_colors = has_multi_colors ? filament_multi_colors->values[i] : std::string(); + FilamentColorMode colorMode = FilamentColorMode::Segment; + if (has_mode) + colorMode = FilamentColorModeFromConfig(filament_colour_modes->values[i]); + multi_color_res.push_back( + FilamentColorUtils::BuildPreprintColorMultiItem(multi_colors, colorMode, filament_color[i])); } response["filament_color"] = number_res; response["filament_color_rgba"] = str_res; + response["filament_color_multi"] = multi_color_res; } diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index e61aa9687ce..ac43ca9a16f 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -27,6 +27,7 @@ #include #include +#include #include #include "libslic3r/libslic3r.h" #include "slic3r/GUI/OptionsGroup.hpp" @@ -5649,21 +5650,47 @@ bool Tab::select_preset(std::string preset_name, bool delete_current /*=false*/, // Orca: update presets for the selected printer if (m_type == Preset::TYPE_PRINTER && wxGetApp().app_config->get_bool("remember_printer_config")) { if (preset_name.find("Snapmaker U1") != std::string::npos) { - // 在 update_selections() 改变耗材数量之前先保存旧数量和颜色 - std::vector old_filament_colors = wxGetApp().plater()->get_extruder_colors_from_plater_config(nullptr, false); - std::vector old_filament_presets = m_preset_bundle->filament_presets; + DynamicPrintConfig& projectConfig = m_preset_bundle->project_config; + std::vector oldFilamentColors = wxGetApp().plater()->get_extruder_colors_from_plater_config(nullptr, false); + std::vector oldFilamentMultiColors; + std::vector oldFilamentColourModes; + if (ConfigOptionStrings* multiColors = projectConfig.option("filament_multi_colors")) + oldFilamentMultiColors = multiColors->values; + if (ConfigOptionInts* modes = projectConfig.option("filament_colour_mode")) + oldFilamentColourModes = modes->values; + + std::vector oldFilamentPresets = m_preset_bundle->filament_presets; + const size_t oldFilamentCount = oldFilamentPresets.size(); + oldFilamentColors.resize(oldFilamentCount, "#26A69A"); + oldFilamentMultiColors.resize(oldFilamentCount); + oldFilamentColourModes.resize(oldFilamentCount, 0); + for (size_t i = 0; i < oldFilamentCount; ++i) + { + if (oldFilamentColors[i].empty()) + oldFilamentColors[i] = "#26A69A"; + if (oldFilamentMultiColors[i].empty()) + oldFilamentMultiColors[i] = oldFilamentColors[i]; + oldFilamentColourModes[i] = oldFilamentColourModes[i] == 1 ? 1 : 0; + } m_preset_bundle->update_selections(*wxGetApp().app_config); - // 恢复耗材预设到原来的数量和预设名称(保持类型自适应) - m_preset_bundle->filament_presets = old_filament_presets; - - // 恢复原来的颜色,保持用户设置的耗材颜色不变 - wxGetApp().preset_bundle->project_config.option("filament_colour")->values = old_filament_colors; - - // 重要:立即保存颜色到配置文件,这样下次切换时也会保持 - std::string filament_colors_str = boost::algorithm::join(old_filament_colors, ","); - wxGetApp().app_config->set_printer_setting(preset_name, "filament_colors", filament_colors_str); + m_preset_bundle->filament_presets = oldFilamentPresets; + + projectConfig.option("filament_colour")->values = oldFilamentColors; + projectConfig.option("filament_multi_colors", true)->values = oldFilamentMultiColors; + projectConfig.option("filament_colour_mode", true)->values = oldFilamentColourModes; + + std::vector filamentColourModeStrings; + filamentColourModeStrings.reserve(oldFilamentColourModes.size()); + for (int mode : oldFilamentColourModes) + filamentColourModeStrings.emplace_back(mode == 1 ? "1" : "0"); + const std::string filamentColors = boost::algorithm::join(oldFilamentColors, ","); + const std::string filamentMultiColors = boost::algorithm::join(oldFilamentMultiColors, ","); + const std::string filamentColourModes = boost::algorithm::join(filamentColourModeStrings, ","); + wxGetApp().app_config->set_printer_setting(preset_name, "filament_colors", filamentColors); + wxGetApp().app_config->set_printer_setting(preset_name, "filament_multi_colors", filamentMultiColors); + wxGetApp().app_config->set_printer_setting(preset_name, "filament_colour_mode", filamentColourModes); wxGetApp().plater()->sidebar().on_filaments_change(m_preset_bundle->filament_presets.size()); } else { diff --git a/src/slic3r/GUI/wxExtensions.cpp b/src/slic3r/GUI/wxExtensions.cpp index 3fca1aa0b91..4a01664aa28 100644 --- a/src/slic3r/GUI/wxExtensions.cpp +++ b/src/slic3r/GUI/wxExtensions.cpp @@ -9,6 +9,7 @@ #include "GUI.hpp" #include "GUI_App.hpp" +#include "FilamentColorUtils.hpp" #include "GUI_ObjectList.hpp" #include "I18N.hpp" #include "GUI_Utils.hpp" @@ -547,12 +548,31 @@ std::vector get_extruder_color_icons(bool thin_icon/* = false*/) Slic3r::GUI::wxGetApp().plater()->get_extruder_colors_from_plater_config(nullptr, false); const size_t num_physical = physical_colors.size(); + const Slic3r::DynamicPrintConfig* config = Slic3r::GUI::wxGetApp().preset_bundle != nullptr ? + &Slic3r::GUI::wxGetApp().preset_bundle->project_config : nullptr; + const Slic3r::ConfigOptionStrings* multiColorOption = config != nullptr && config->has("filament_multi_colors") ? + config->option("filament_multi_colors") : nullptr; + const Slic3r::ConfigOptionInts* modeOption = config != nullptr && config->has("filament_colour_mode") ? + config->option("filament_colour_mode") : nullptr; + int index = 0; for (const std::string &color : colors) { auto label = std::to_string(++index); const size_t color_idx = size_t(index) - 1; + if (color_idx < num_physical) { + const std::string multiColors = multiColorOption != nullptr && multiColorOption->values.size() > color_idx ? + multiColorOption->values[color_idx] : std::string(); + int modeValue = 0; + if (modeOption != nullptr && modeOption->values.size() > color_idx) + modeValue = modeOption->values[color_idx]; + const Slic3r::FilamentColorMode colorMode = Slic3r::FilamentColorModeFromConfig(modeValue); + bmps.push_back(Slic3r::GUI::FilamentColorUtils::GetFilamentColorIcon(multiColors, colorMode, color, label, + icon_width, icon_height)); + continue; + } + if (color_idx >= num_physical) { // Mixed filament — try to draw a gradient icon. const size_t mixed_idx = color_idx - num_physical; From 21a7386122b5b918126e570af8edaa24739966d1 Mon Sep 17 00:00:00 2001 From: xiejiajun <413189257@qq.com> Date: Thu, 18 Jun 2026 18:28:36 +0800 Subject: [PATCH 2/2] style : rename FilamentColorLibrary member variables for clarity Rename private member variables to better indicate their types: - _filaments -> _filamentInfoVec - _filamentIndexById -> _filamentIndexByIdMap - _filamentIndexByName -> _filamentIndexByNameMap --- src/libslic3r/FilamentColorLibrary.cpp | 24 ++++++++++++------------ src/libslic3r/FilamentColorLibrary.hpp | 6 +++--- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/libslic3r/FilamentColorLibrary.cpp b/src/libslic3r/FilamentColorLibrary.cpp index d338287a8fa..98e5d82c8dc 100644 --- a/src/libslic3r/FilamentColorLibrary.cpp +++ b/src/libslic3r/FilamentColorLibrary.cpp @@ -353,8 +353,8 @@ bool FilamentColorLibrary::FindFilamentById(const std::string& filamentId, Filam if (!EnsureLoaded()) return false; - std::unordered_map::const_iterator filamentIt = _filamentIndexById.find(filamentId); - if (filamentIt == _filamentIndexById.end()) + std::unordered_map::const_iterator filamentIt = _filamentIndexByIdMap.find(filamentId); + if (filamentIt == _filamentIndexByIdMap.end()) return false; return FindFilamentByIndex(filamentIt->second, outFilament); @@ -369,16 +369,16 @@ bool FilamentColorLibrary::FindFilamentByName(const std::string& filamentName, F if (matchName.empty()) return false; - std::unordered_map::const_iterator nameIt = _filamentIndexByName.find(matchName); - return nameIt != _filamentIndexByName.end() ? FindFilamentByIndex(nameIt->second, outFilament) : false; + std::unordered_map::const_iterator nameIt = _filamentIndexByNameMap.find(matchName); + return nameIt != _filamentIndexByNameMap.end() ? FindFilamentByIndex(nameIt->second, outFilament) : false; } bool FilamentColorLibrary::FindFilamentByIndex(size_t index, FilamentColorInfo& outFilament) const { - if (index >= _filaments.size()) + if (index >= _filamentInfoVec.size()) return false; - outFilament = _filaments[index]; + outFilament = _filamentInfoVec[index]; return true; } @@ -522,17 +522,17 @@ bool FilamentColorLibrary::LoadIndex() return false; } - _filaments = std::move(filaments); - _filamentIndexById = std::move(filamentIndexById); - _filamentIndexByName = std::move(filamentIndexByName); + _filamentInfoVec = std::move(filaments); + _filamentIndexByIdMap = std::move(filamentIndexById); + _filamentIndexByNameMap = std::move(filamentIndexByName); return true; } void FilamentColorLibrary::Clear() { - _filaments.clear(); - _filamentIndexById.clear(); - _filamentIndexByName.clear(); + _filamentInfoVec.clear(); + _filamentIndexByIdMap.clear(); + _filamentIndexByNameMap.clear(); } } // namespace Slic3r diff --git a/src/libslic3r/FilamentColorLibrary.hpp b/src/libslic3r/FilamentColorLibrary.hpp index 276e2c1d97e..349a7b66556 100644 --- a/src/libslic3r/FilamentColorLibrary.hpp +++ b/src/libslic3r/FilamentColorLibrary.hpp @@ -74,9 +74,9 @@ class FilamentColorLibrary private: bool _loaded { false }; - std::vector _filaments; - std::unordered_map _filamentIndexById; - std::unordered_map _filamentIndexByName; + std::vector _filamentInfoVec; + std::unordered_map _filamentIndexByIdMap; // filament_id to index in _filamentInfoVec + std::unordered_map _filamentIndexByNameMap; // normalized filament name to index in _filamentInfoVec }; } // namespace Slic3r