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