diff --git a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po index 21e7addf59b..a4178e9160a 100644 --- a/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po +++ b/localization/i18n/zh_CN/Snapmaker_Orca_zh_CN.po @@ -15279,3 +15279,12 @@ msgstr "允许高/低温材料混合打印" msgid "If enabled, allows printing with both high-temperature and low-temperature filaments simultaneously." msgstr "启用后允许高温材料与低温材料同时打印" + +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..98e5d82c8dc --- /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 = _filamentIndexByIdMap.find(filamentId); + if (filamentIt == _filamentIndexByIdMap.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 = _filamentIndexByNameMap.find(matchName); + return nameIt != _filamentIndexByNameMap.end() ? FindFilamentByIndex(nameIt->second, outFilament) : false; +} + +bool FilamentColorLibrary::FindFilamentByIndex(size_t index, FilamentColorInfo& outFilament) const +{ + if (index >= _filamentInfoVec.size()) + return false; + + outFilament = _filamentInfoVec[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; + } + + _filamentInfoVec = std::move(filaments); + _filamentIndexByIdMap = std::move(filamentIndexById); + _filamentIndexByNameMap = std::move(filamentIndexByName); + return true; +} + +void FilamentColorLibrary::Clear() +{ + _filamentInfoVec.clear(); + _filamentIndexByIdMap.clear(); + _filamentIndexByNameMap.clear(); +} + +} // namespace Slic3r diff --git a/src/libslic3r/FilamentColorLibrary.hpp b/src/libslic3r/FilamentColorLibrary.hpp new file mode 100644 index 00000000000..349a7b66556 --- /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 _filamentInfoVec; + std::unordered_map _filamentIndexByIdMap; // filament_id to index in _filamentInfoVec + std::unordered_map _filamentIndexByNameMap; // normalized filament name to index in _filamentInfoVec +}; + +} // 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 f5eb30af3c6..703e8ed32db 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 66b0c29a7ba..c4881c23c9a 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"); @@ -6449,6 +6465,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 9f669e30c81..68d18bccf2c 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1338,6 +1338,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 4afcead1c5d..1d0b0618c3c 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -10162,6 +10162,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 b728c1601c3..7734f40818c 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" @@ -5652,21 +5653,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;