Compare commits

..

No commits in common. "d6ad49d9cb5e95753162fdd7a23bcf0434c73275" and "ecdcafc9677f7ab3702dec7d1b1985640541e714" have entirely different histories.

9 changed files with 60 additions and 629 deletions

View file

@ -27,7 +27,6 @@ A self-hosted web app for organizing your 3D-printing files (STL, 3MF, OBJ, FBX,
- **AI tags & descriptions** — bring your own key for DeepSeek (the default), OpenRouter, OpenAI or any OpenAI-compatible server (Ollama, LM Studio). Tag the whole library, only models not yet tagged, or a single model from its page. Each model's name, file names and (optionally) thumbnail are sent; tags are only ever added and descriptions only filled where empty unless you ask. Optionally it also gives models with unclear names (file-name leftovers, typos, site boilerplate) a clearer one; the old name is kept and can be restored with Undo on the model page. It can also suggest collections (reusing yours where they fit): suggestions wait on the AI page, grouped by name, until you create/add or dismiss them. Each run can do any mix of tags & descriptions, names and collections, and a reply that isn't the JSON asked for is retried once. Runs show live progress, tokens and cost, can be stopped and resumed, stop at an optional spending limit, and pause (rather than keep spending) if the server restarts. "Save & test connection" in Settings checks the key, model and whether it accepts images.
- **Search by meaning** — switch the search box to *Meaning* to find models by what they are ("wall hook" finds a "coat hanger bracket"). Uses a small embedding model (bge-small-en-v1.5, ~34 MB) that runs on the server, so it's free and private; the index updates in the background as models change.
- **Automatic tags** — strict whole-word rules over names, file names and archive contents (part types, mechanisms, materials, techniques) plus your own "word → tag" rules. Switch it off (optionally removing its tags) or re-apply it any time. Tags you add yourself are never changed; AI and rule tags are marked on the model page.
- **Filament & what's printable** — keep the spools you own (material, colour, brand, grams left) on the **Filament** page. Bambu Studio / OrcaSlicer projects name the filament in each slot, which Printhive reads once per model in the background, so cards and the model page say whether you can print it now and what you're short of, and the toolbar can show only what's printable. Each slot is matched to a spool by material *and* colour — a four-colour print needs those four colours, and one spool can't fill two slots. Near shades count as the same colour; the model page lists every slot, which spool fills it, and the nearest thing you own when none does. Models that aren't slicer projects don't name their filaments, and say so rather than being called unprintable. Adding a spool can start from Bambu Lab's own filament list — pick "PLA Basic — Jade White (10100)" and the material, colour and code fill themselves — and slot colours are named where they match it. That list is included by kind permission of [3D Filament Profiles](https://3dfilamentprofiles.com), whose terms otherwise reserve it; ask them before reusing `db/bambu-filaments.json` elsewhere.
- **Trash** — deleting moves a model to the trash; restore it, delete it forever, or empty the trash (only then are files removed from disk).
- **Settings** — API tokens / FlareSolverr URL stored in the database (with `.env` fallback), secrets masked.

View file

@ -1,334 +0,0 @@
{
"source": "https://3dfilamentprofiles.com/filaments/bambu-lab",
"permission": "Copied with the site owner's permission, granted on request 2026-09-17. Their terms otherwise prohibit copying, scraping and redistribution — ask them before reusing this file anywhere else.",
"collected": "2026-09-17",
"count": 325,
"note": "Bambu Lab filaments: material, product type, colour name, Bambu filament code (what the AMS reports), and the colour(s) shown. Multi-colour filaments list each colour; a few entries have no published code.",
"filaments": [
{"material":"ABS","type":"Basic","name":"Azure","code":"40601","colours":["#489FDF"]},
{"material":"ABS","type":"Basic","name":"Bambu Green","code":"40500","colours":["#00AE42"]},
{"material":"ABS","type":"Basic","name":"Beige","code":"40401","colours":["#DFD1A7"]},
{"material":"ABS","type":"Basic","name":"Black","code":"40101","colours":["#000000"]},
{"material":"ABS","type":"Basic","name":"Blue","code":"40600","colours":["#0A2CA5"]},
{"material":"ABS","type":"Basic","name":"Grey","code":"10103","colours":["#8E9089"]},
{"material":"ABS","type":"Basic","name":"Lavender","code":"40701","colours":["#7248BD"]},
{"material":"ABS","type":"Basic","name":"Mint","code":"40501","colours":["#ADF4DC"]},
{"material":"ABS","type":"Basic","name":"Navy Blue","code":"40602","colours":["#0C2340"]},
{"material":"ABS","type":"Basic","name":"Olive","code":"40502","colours":["#789D4A"]},
{"material":"ABS","type":"Basic","name":"Orange","code":"40300","colours":["#FF6A13"]},
{"material":"ABS","type":"Basic","name":"Purple","code":"40700","colours":["#AF1685"]},
{"material":"ABS","type":"Basic","name":"Red","code":"40200","colours":["#D32941"]},
{"material":"ABS","type":"Basic","name":"Silver","code":"40102","colours":["#87909A"]},
{"material":"ABS","type":"Basic","name":"Tangerine Yellow","code":"40402","colours":["#FFC72C"]},
{"material":"ABS","type":"Basic","name":"White","code":"40100","colours":["#FFFFFF"]},
{"material":"ABS","type":"Basic","name":"Yellow","code":"40400","colours":["#FFFF20"]},
{"material":"ABS","type":"GF","name":"Black","code":"41101","colours":["#000000"]},
{"material":"ABS","type":"GF","name":"Blue","code":"41600","colours":["#0C3B95"]},
{"material":"ABS","type":"GF","name":"Gray","code":"41102","colours":["#C6C6C6"]},
{"material":"ABS","type":"GF","name":"Green","code":"41500","colours":["#61BF36"]},
{"material":"ABS","type":"GF","name":"Orange","code":"41300","colours":["#F48438"]},
{"material":"ABS","type":"GF","name":"Red","code":"41200","colours":["#E83100"]},
{"material":"ABS","type":"GF","name":"White","code":"41100","colours":["#FFFFFF"]},
{"material":"ABS","type":"GF","name":"Yellow","code":"41400","colours":["#FFE133"]},
{"material":"ASA","type":"Aero","name":"White","code":"46100","colours":["#EFF2DE"]},
{"material":"ASA","type":"Basic","name":"Black","code":"45101","colours":["#000000"]},
{"material":"ASA","type":"Basic","name":"Blue","code":"45600","colours":["#2140B4"]},
{"material":"ASA","type":"Basic","name":"Gray","code":"45102","colours":["#8A949E"]},
{"material":"ASA","type":"Basic","name":"Green","code":"45500","colours":["#00A6A0"]},
{"material":"ASA","type":"Basic","name":"Red","code":"45200","colours":["#E02928"]},
{"material":"ASA","type":"Basic","name":"White","code":"45100","colours":["#FFFAF2"]},
{"material":"ASA","type":"CF","name":"Black","code":"46101","colours":["#000000"]},
{"material":"HIPS","type":"Support","name":"Support for ABS White","code":"66100","colours":["#FFFFFF"]},
{"material":"PA6","type":"CF","name":"Black","code":"72100","colours":["#000000"]},
{"material":"PA6","type":"GF","name":"Black","code":"72104","colours":["#000000"]},
{"material":"PA6","type":"GF","name":"Blue","code":"72600","colours":["#75AED8"]},
{"material":"PA6","type":"GF","name":"Brown","code":"72800","colours":["#5B492F"]},
{"material":"PA6","type":"GF","name":"Gray","code":"72103","colours":["#353533"]},
{"material":"PA6","type":"GF","name":"Lime","code":"72500","colours":["#C5ED48"]},
{"material":"PA6","type":"GF","name":"Orange","code":"72200","colours":["#FF4800"]},
{"material":"PA6","type":"GF","name":"White","code":"72102","colours":["#EAEAE4"]},
{"material":"PA6","type":"GF","name":"Yellow","code":"72400","colours":["#FFCE00"]},
{"material":"PAHT","type":"CF","name":"Black","code":"70100","colours":["#000000"]},
{"material":"PAHT","type":"Support","name":"Support for PA/PET","code":"65500","colours":["#C0DF16"]},
{"material":"PC","type":"Basic","name":"Black","code":"60101","colours":["#000000"]},
{"material":"PC","type":"Basic","name":"Clear Black","code":"60102","colours":["#5A5161"]},
{"material":"PC","type":"Basic","name":"Transparent","code":"60103","colours":["#BFC3C7"]},
{"material":"PC","type":"Basic","name":"White","code":"60100","colours":["#FFFFFF"]},
{"material":"PC","type":"FR","name":"Black","code":"63100","colours":["#000000"]},
{"material":"PC","type":"FR","name":"Gray","code":"63102","colours":["#A8A8AA"]},
{"material":"PC","type":"FR","name":"White","code":"63101","colours":["#FFFFFF"]},
{"material":"PET","type":"CF","name":"Black","code":"71100","colours":["#000000"]},
{"material":"PETG","type":"Basic","name":"Black","code":"30101","colours":["#000000"]},
{"material":"PETG","type":"Basic","name":"Black","code":"30105","colours":["#000000"]},
{"material":"PETG","type":"Basic","name":"Blue","code":"30600","colours":["#001489"]},
{"material":"PETG","type":"Basic","name":"Blue Gray","code":"30601","colours":["#2E4F66"]},
{"material":"PETG","type":"Basic","name":"Dark Beige","code":"30403","colours":["#DBC8B6"]},
{"material":"PETG","type":"Basic","name":"Dark Brown","code":"30800","colours":["#4F2C1D"]},
{"material":"PETG","type":"Basic","name":"Gold","code":"30401","colours":["#F6D86A"]},
{"material":"PETG","type":"Basic","name":"Gray","code":"30102","colours":["#ADB1B2"]},
{"material":"PETG","type":"Basic","name":"Gray","code":"30107","colours":["#7F7E83"]},
{"material":"PETG","type":"Basic","name":"Green","code":"30500","colours":["#009639"]},
{"material":"PETG","type":"Basic","name":"Green","code":"30502","colours":["#009639"]},
{"material":"PETG","type":"Basic","name":"Lake Blue","code":"30602","colours":["#0069B1"]},
{"material":"PETG","type":"Basic","name":"Lime Green","code":"30501","colours":["#7CD82B"]},
{"material":"PETG","type":"Basic","name":"Misty Blue","code":"30108","colours":["#688197"]},
{"material":"PETG","type":"Basic","name":"Nature","code":"30104","colours":["#D7D7D7"]},
{"material":"PETG","type":"Basic","name":"Navy Blue","code":"30604","colours":["#0086D6"]},
{"material":"PETG","type":"Basic","name":"Orange","code":"30300","colours":["#FF671F"]},
{"material":"PETG","type":"Basic","name":"Orange","code":"30301","colours":["#FF671F"]},
{"material":"PETG","type":"Basic","name":"Pine Green","code":"30503","colours":["#034638"]},
{"material":"PETG","type":"Basic","name":"Purple","code":"30700","colours":["#9E007E"]},
{"material":"PETG","type":"Basic","name":"Red","code":"30200","colours":["#D6001C"]},
{"material":"PETG","type":"Basic","name":"Red","code":"30201","colours":["#D6001C"]},
{"material":"PETG","type":"Basic","name":"Reflex Blue","code":"30603","colours":["#001489"]},
{"material":"PETG","type":"Basic","name":"Translucent","code":"30103","colours":["#FFFFFF"]},
{"material":"PETG","type":"Basic","name":"White","code":"30100","colours":["#EDF0F2"]},
{"material":"PETG","type":"Basic","name":"White","code":"30106","colours":["#FFFFFF"]},
{"material":"PETG","type":"Basic","name":"Yellow","code":"30400","colours":["#FCE300"]},
{"material":"PETG","type":"Basic","name":"Yellow","code":"30402","colours":["#FCE300"]},
{"material":"PETG","type":"CF","name":"Black","code":"31100","colours":["#000000"]},
{"material":"PETG","type":"CF","name":"Brick Red","code":"31200","colours":["#9F332A"]},
{"material":"PETG","type":"CF","name":"Indigo Blue","code":"31600","colours":["#324585"]},
{"material":"PETG","type":"CF","name":"Malachite Green","code":"31500","colours":["#16B08E"]},
{"material":"PETG","type":"CF","name":"Titan Gray","code":"31101","colours":["#565656"]},
{"material":"PETG","type":"CF","name":"Violet Purple","code":"31700","colours":["#583061"]},
{"material":"PETG","type":"HF","name":"Black","code":"33102","colours":["#000000"]},
{"material":"PETG","type":"HF","name":"Blue","code":"33600","colours":["#002E96"]},
{"material":"PETG","type":"HF","name":"Cream","code":"33401","colours":["#F9DFB9"]},
{"material":"PETG","type":"HF","name":"Dark Gray","code":"33103","colours":["#515151"]},
{"material":"PETG","type":"HF","name":"Forest Green","code":"33502","colours":["#39541A"]},
{"material":"PETG","type":"HF","name":"Gray","code":"33101","colours":["#ADB1B2"]},
{"material":"PETG","type":"HF","name":"Green","code":"33500","colours":["#00AE42"]},
{"material":"PETG","type":"HF","name":"Lake Blue","code":"33601","colours":["#1F79E5"]},
{"material":"PETG","type":"HF","name":"Lime Green","code":"33501","colours":["#6EE53C"]},
{"material":"PETG","type":"HF","name":"Orange","code":"33300","colours":["#F75403"]},
{"material":"PETG","type":"HF","name":"Peanut Brown","code":"33801","colours":["#875718"]},
{"material":"PETG","type":"HF","name":"Red","code":"33200","colours":["#EB3A3A"]},
{"material":"PETG","type":"HF","name":"White","code":"33100","colours":["#FFFFFF"]},
{"material":"PETG","type":"HF","name":"Yellow","code":"33400","colours":["#FFD00B"]},
{"material":"PETG","type":"Translucent","name":"Brown","code":"32800","colours":["#C9A381"]},
{"material":"PETG","type":"Translucent","name":"Clear","code":"32101","colours":["#EBEBEB"]},
{"material":"PETG","type":"Translucent","name":"Gray","code":"32100","colours":["#8E8E8E"]},
{"material":"PETG","type":"Translucent","name":"Light Blue","code":"32600","colours":["#61B0FF"]},
{"material":"PETG","type":"Translucent","name":"Olive","code":"32500","colours":["#748C45"]},
{"material":"PETG","type":"Translucent","name":"Orange","code":"32300","colours":["#FF911A"]},
{"material":"PETG","type":"Translucent","name":"Pink","code":"32200","colours":["#F9C1BD"]},
{"material":"PETG","type":"Translucent","name":"Purple","code":"32700","colours":["#D6ABFF"]},
{"material":"PETG","type":"Translucent","name":"Teal","code":"32501","colours":["#77EDD7"]},
{"material":"PLA","type":"Aero","name":"Black","code":"14103","colours":["#000000"]},
{"material":"PLA","type":"Aero","name":"Gray","code":"14104","colours":["#CDCECA"]},
{"material":"PLA","type":"Aero","name":"White","code":"14102","colours":["#FFFFFF"]},
{"material":"PLA","type":"Basic","name":"Bambu Green","code":"10501","colours":["#00AE42"]},
{"material":"PLA","type":"Basic","name":"Beige","code":"10201","colours":["#F7E6DE"]},
{"material":"PLA","type":"Basic","name":"Black","code":"10101","colours":["#000000"]},
{"material":"PLA","type":"Basic","name":"Blue","code":"10600","colours":["#0A2989"]},
{"material":"PLA","type":"Basic","name":"Blue","code":"10601","colours":["#0A2989"]},
{"material":"PLA","type":"Basic","name":"Blue Gray","code":"10602","colours":["#5B6579"]},
{"material":"PLA","type":"Basic","name":"Bright Green","code":"10503","colours":["#BECF00"]},
{"material":"PLA","type":"Basic","name":"Bronze","code":"10801","colours":["#847D48"]},
{"material":"PLA","type":"Basic","name":"Brown","code":"10800","colours":["#9D432C"]},
{"material":"PLA","type":"Basic","name":"Cobalt Blue","code":"10604","colours":["#0056B8"]},
{"material":"PLA","type":"Basic","name":"Cocoa Brown","code":"10802","colours":["#6F5034"]},
{"material":"PLA","type":"Basic","name":"Cyan","code":"10603","colours":["#0086D6"]},
{"material":"PLA","type":"Basic","name":"Dark Gray","code":"10105","colours":["#545454"]},
{"material":"PLA","type":"Basic","name":"Gold","code":"10401","colours":["#E4BD68"]},
{"material":"PLA","type":"Basic","name":"Gray","code":"10103","colours":["#8E9089"]},
{"material":"PLA","type":"Basic","name":"Green","code":"10500","colours":["#002914"]},
{"material":"PLA","type":"Basic","name":"Hot Pink","code":"10204","colours":["#F5547C"]},
{"material":"PLA","type":"Basic","name":"Indigo Purple","code":"10701","colours":["#482960"]},
{"material":"PLA","type":"Basic","name":"Jade White","code":"10100","colours":["#FFFFFF"]},
{"material":"PLA","type":"Basic","name":"Light Gray","code":"10104","colours":["#D1D3D5"]},
{"material":"PLA","type":"Basic","name":"Magenta","code":"10202","colours":["#EC008C"]},
{"material":"PLA","type":"Basic","name":"Maroon Red","code":"10205","colours":["#9D2235"]},
{"material":"PLA","type":"Basic","name":"Mistletoe Green","code":"10502","colours":["#3F8E43"]},
{"material":"PLA","type":"Basic","name":"Orange","code":"10300","colours":["#FF6A13"]},
{"material":"PLA","type":"Basic","name":"Pink","code":"10203","colours":["#F55A74"]},
{"material":"PLA","type":"Basic","name":"Pumpkin Orange","code":"10301","colours":["#FF9016"]},
{"material":"PLA","type":"Basic","name":"Purple","code":"10700","colours":["#5E43B7"]},
{"material":"PLA","type":"Basic","name":"Red","code":"10200","colours":["#C12E1F"]},
{"material":"PLA","type":"Basic","name":"Silver","code":"10102","colours":["#A6A9AA"]},
{"material":"PLA","type":"Basic","name":"Sunflower Yellow","code":"10402","colours":["#FEC600"]},
{"material":"PLA","type":"Basic","name":"Turquoise","code":"10605","colours":["#00B1B7"]},
{"material":"PLA","type":"Basic","name":"Yellow","code":"10400","colours":["#F4EE2A"]},
{"material":"PLA","type":"CF","name":"Black","code":"14100","colours":["#000000"]},
{"material":"PLA","type":"CF","name":"Burgundy Red","code":"14200","colours":["#951E23"]},
{"material":"PLA","type":"CF","name":"Iris Purple","code":"14700","colours":["#69398E"]},
{"material":"PLA","type":"CF","name":"Jeans Blue","code":"14600","colours":["#6E88BC"]},
{"material":"PLA","type":"CF","name":"Lava Gray","code":"14101","colours":["#4D5054"]},
{"material":"PLA","type":"CF","name":"Matcha Green","code":"14500","colours":["#5C9748"]},
{"material":"PLA","type":"CF","name":"Royal Blue","code":"14601","colours":["#2842AD"]},
{"material":"PLA","type":"CMYK","name":"Lithophane Bundle","code":"","colours":["#00FFFF","#FFFF00","#FF00FF","#FFFFFF"]},
{"material":"PLA","type":"Galaxy","name":"Brown","code":"13203","colours":["#684A43"]},
{"material":"PLA","type":"Galaxy","name":"Green","code":"13503","colours":["#3B665E"]},
{"material":"PLA","type":"Galaxy","name":"Nebula","code":"13504","colours":["#424379"]},
{"material":"PLA","type":"Galaxy","name":"Purple","code":"13602","colours":["#594177"]},
{"material":"PLA","type":"Glow","name":"Glow Blue","code":"15600","colours":["#7AC0E9"]},
{"material":"PLA","type":"Glow","name":"Glow Green","code":"15500","colours":["#A1FFAC"]},
{"material":"PLA","type":"Glow","name":"Glow Orange","code":"15300","colours":["#FF9D5B"]},
{"material":"PLA","type":"Glow","name":"Glow Pink","code":"15200","colours":["#F17B8F"]},
{"material":"PLA","type":"Glow","name":"Glow Yellow","code":"15400","colours":["#F8FF80"]},
{"material":"PLA","type":"Gradient","name":"Arctic Whisper","code":"10900","colours":["#9CDBD9","#FFFFFF"]},
{"material":"PLA","type":"Gradient","name":"Blueberry Bubblegum","code":"10905","colours":["#6FCAEF","#8573DD"]},
{"material":"PLA","type":"Gradient","name":"Cotton Candy Cloud","code":"10907","colours":["#E7C1D5","#8EC9E9"]},
{"material":"PLA","type":"Gradient","name":"Dusk Glare","code":"10906","colours":["#ED6022","#FFCBA6"]},
{"material":"PLA","type":"Gradient","name":"Mint Lime","code":"10904","colours":["#4EC83A","#D7FE9E"]},
{"material":"PLA","type":"Gradient","name":"Ocean to Meadow","code":"10902","colours":["#307FE2","#54FF9B"]},
{"material":"PLA","type":"Gradient","name":"Pink Citrus","code":"10903","colours":["#E4555E","#FCDAD2"]},
{"material":"PLA","type":"Gradient","name":"Solar Breeze","code":"10901","colours":["#FC776A","#FFF7F5"]},
{"material":"PLA","type":"Lite","name":"Beige","code":"16700","colours":["#F7E6DE"]},
{"material":"PLA","type":"Lite","name":"Black","code":"16100","colours":["#000000"]},
{"material":"PLA","type":"Lite","name":"Blue","code":"16601","colours":["#004EA8"]},
{"material":"PLA","type":"Lite","name":"Cocoa Brown","code":"","colours":["#8E3C06"]},
{"material":"PLA","type":"Lite","name":"Cyan","code":"16600","colours":["#00FFFF"]},
{"material":"PLA","type":"Lite","name":"Dark Gray","code":"16102","colours":["#6F6E6D"]},
{"material":"PLA","type":"Lite","name":"Gray","code":"16101","colours":["#9FA19F"]},
{"material":"PLA","type":"Lite","name":"Green","code":"16501","colours":["#00BB31"]},
{"material":"PLA","type":"Lite","name":"Orange","code":"16301","colours":["#FF671F"]},
{"material":"PLA","type":"Lite","name":"Red","code":"16200","colours":["#FF0000"]},
{"material":"PLA","type":"Lite","name":"Sunflower Yellow","code":"16401","colours":["#FFB549"]},
{"material":"PLA","type":"Lite","name":"White","code":"16103","colours":["#FFFEF7"]},
{"material":"PLA","type":"Lite","name":"Yellow","code":"16400","colours":["#FFD834"]},
{"material":"PLA","type":"Marble","name":"Red Granite","code":"13201","colours":["#AD4E38"]},
{"material":"PLA","type":"Marble","name":"White Marble","code":"13103","colours":["#F7F3F0"]},
{"material":"PLA","type":"Matte","name":"Apple Green","code":"11502","colours":["#C2E189"]},
{"material":"PLA","type":"Matte","name":"Ash Gray","code":"11102","colours":["#9B9EA0"]},
{"material":"PLA","type":"Matte","name":"Bone White","code":"11103","colours":["#CBC6B8"]},
{"material":"PLA","type":"Matte","name":"Caramel","code":"11803","colours":["#AE835B"]},
{"material":"PLA","type":"Matte","name":"Charcoal","code":"11101","colours":["#000000"]},
{"material":"PLA","type":"Matte","name":"Dark Blue","code":"11602","colours":["#042F56"]},
{"material":"PLA","type":"Matte","name":"Dark Brown","code":"11801","colours":["#7D6556"]},
{"material":"PLA","type":"Matte","name":"Dark Chocolate","code":"11802","colours":["#4D3324"]},
{"material":"PLA","type":"Matte","name":"Dark Green","code":"11501","colours":["#68724D"]},
{"material":"PLA","type":"Matte","name":"Dark Red","code":"11202","colours":["#BB3D43"]},
{"material":"PLA","type":"Matte","name":"Desert Tan","code":"11401","colours":["#E8DBB7"]},
{"material":"PLA","type":"Matte","name":"Grass Green","code":"11500","colours":["#61C680"]},
{"material":"PLA","type":"Matte","name":"Ice Blue","code":"11601","colours":["#A3D8E1"]},
{"material":"PLA","type":"Matte","name":"Ivory White","code":"11100","colours":["#FFFFFF"]},
{"material":"PLA","type":"Matte","name":"Latte Brown","code":"11800","colours":["#D3B7A7"]},
{"material":"PLA","type":"Matte","name":"Lemon Yellow","code":"11400","colours":["#F7D959"]},
{"material":"PLA","type":"Matte","name":"Lilac Purple","code":"11700","colours":["#AE96D4"]},
{"material":"PLA","type":"Matte","name":"Mandarin Orange","code":"11300","colours":["#F99963"]},
{"material":"PLA","type":"Matte","name":"Marine Blue","code":"11600","colours":["#0078BF"]},
{"material":"PLA","type":"Matte","name":"Nardo Gray","code":"11104","colours":["#757575"]},
{"material":"PLA","type":"Matte","name":"Plum","code":"11204","colours":["#950051"]},
{"material":"PLA","type":"Matte","name":"Sakura Pink","code":"11201","colours":["#E8AFCF"]},
{"material":"PLA","type":"Matte","name":"Scarlet Red","code":"11200","colours":["#DE4343"]},
{"material":"PLA","type":"Matte","name":"Sky Blue","code":"11603","colours":["#56B7E6"]},
{"material":"PLA","type":"Matte","name":"Terracotta","code":"11203","colours":["#B15533"]},
{"material":"PLA","type":"Metal","name":"Cobalt Blue Metallic","code":"13600","colours":["#39699E"]},
{"material":"PLA","type":"Metal","name":"Copper Brown Metallic","code":"13800","colours":["#AA6443"]},
{"material":"PLA","type":"Metal","name":"Iridium Gold Metallic","code":"13400","colours":["#B39B84"]},
{"material":"PLA","type":"Metal","name":"Iron Gray Metallic","code":"13100","colours":["#43403D"]},
{"material":"PLA","type":"Metal","name":"Oxide Green Metallic","code":"13500","colours":["#1D7C6A"]},
{"material":"PLA","type":"Pure","name":"Absolute Black","code":"17101","colours":["#000000"]},
{"material":"PLA","type":"Pure","name":"Apricot","code":"17300","colours":["#FFB673"]},
{"material":"PLA","type":"Pure","name":"Baby Blue","code":"17600","colours":["#A4DBE8"]},
{"material":"PLA","type":"Pure","name":"Milky Pink","code":"17200","colours":["#F7CED7"]},
{"material":"PLA","type":"Pure","name":"Pure White","code":"17100","colours":["#FFFFFF"]},
{"material":"PLA","type":"Silk","name":"Blue","code":"13601","colours":["#147BD1"]},
{"material":"PLA","type":"Silk","name":"Copper","code":"13300","colours":["#5E4B3C"]},
{"material":"PLA","type":"Silk","name":"Gold","code":"13401","colours":["#E5B03D"]},
{"material":"PLA","type":"Silk","name":"Green","code":"13502","colours":["#4CE4A0"]},
{"material":"PLA","type":"Silk","name":"Pink","code":"13202","colours":["#EEB1C1"]},
{"material":"PLA","type":"Silk","name":"Purple","code":"13701","colours":["#580490"]},
{"material":"PLA","type":"Silk","name":"Silver","code":"13104","colours":["#ACB4B5"]},
{"material":"PLA","type":"Silk","name":"White","code":"13105","colours":["#FFFFFF"]},
{"material":"PLA","type":"Silk Multi-Color","name":"Aurora Purple","code":"13909","colours":["#7F3696","#006EC9"]},
{"material":"PLA","type":"Silk Multi-Color","name":"Blue Hawaii (Blue-Green)","code":"13904","colours":["#60A4E8","#4CE4A0"]},
{"material":"PLA","type":"Silk Multi-Color","name":"Dawn Radiance","code":"13912","colours":["#EC984C","#6CD4BC","#A66EB9","#D87694"]},
{"material":"PLA","type":"Silk Multi-Color","name":"Gilded Rose (Pink-Gold)","code":"13901","colours":["#FF9425","#FCA2BF"]},
{"material":"PLA","type":"Silk Multi-Color","name":"Midnight Blaze (Blue-Red)","code":"13902","colours":["#0047BB","#7D1B49"]},
{"material":"PLA","type":"Silk Multi-Color","name":"Mystic Magenta (Purple+Green)","code":"13913","colours":["#720062","#3A913F"]},
{"material":"PLA","type":"Silk Multi-Color","name":"Neon City (Blue-Magenta)","code":"13903","colours":["#0047BB","#BB22A3"]},
{"material":"PLA","type":"Silk Multi-Color","name":"Phantom Blue (Blue-Black)","code":"13916","colours":["#000000","#00629B"]},
{"material":"PLA","type":"Silk Multi-Color","name":"South Beach","code":"13906","colours":["#F772A4","#00918B"]},
{"material":"PLA","type":"Silk Multi-Color","name":"Velvet Eclipse (Black-Red)","code":"13905","colours":["#000000","#C80000"]},
{"material":"PLA","type":"Silk+","name":"Baby Blue","code":"13603","colours":["#A8C6EE"]},
{"material":"PLA","type":"Silk+","name":"Blue","code":"13604","colours":["#008BDA"]},
{"material":"PLA","type":"Silk+","name":"Candy Green","code":"13506","colours":["#018814"]},
{"material":"PLA","type":"Silk+","name":"Candy Red","code":"13205","colours":["#D02727"]},
{"material":"PLA","type":"Silk+","name":"Champagne","code":"13404","colours":["#F3CFB2"]},
{"material":"PLA","type":"Silk+","name":"Gold","code":"13405","colours":["#F4A925"]},
{"material":"PLA","type":"Silk+","name":"Mint","code":"13507","colours":["#96DCB9"]},
{"material":"PLA","type":"Silk+","name":"Pink","code":"13207","colours":["#F7ADA6"]},
{"material":"PLA","type":"Silk+","name":"Purple","code":"13702","colours":["#8671CB"]},
{"material":"PLA","type":"Silk+","name":"Rose Gold","code":"13206","colours":["#BA9594"]},
{"material":"PLA","type":"Silk+","name":"Silver","code":"13109","colours":["#C8C8C8"]},
{"material":"PLA","type":"Silk+","name":"Titan Gray","code":"13108","colours":["#5F6367"]},
{"material":"PLA","type":"Silk+","name":"White","code":"13110","colours":["#FFFFFF"]},
{"material":"PLA","type":"Sparkle","name":"Alpine Green Sparkle","code":"13501","colours":["#3F5443"]},
{"material":"PLA","type":"Sparkle","name":"Classic Gold Sparkle","code":"13402","colours":["#CEA629"]},
{"material":"PLA","type":"Sparkle","name":"Crimson Red Sparkle","code":"13200","colours":["#792B36"]},
{"material":"PLA","type":"Sparkle","name":"Onyx Black Sparkle","code":"13101","colours":["#2D2B28"]},
{"material":"PLA","type":"Sparkle","name":"Royal Purple Sparkle","code":"13700","colours":["#483D8B"]},
{"material":"PLA","type":"Sparkle","name":"Slate Gray Sparkle","code":"13102","colours":["#8E9089"]},
{"material":"PLA","type":"Support","name":"Support for PLA Black","code":"65101","colours":["#000000"]},
{"material":"PLA","type":"Support","name":"Support for PLA White","code":"65100","colours":["#FFFFFF"]},
{"material":"PLA","type":"Support","name":"Support for PLA White","code":"65104","colours":["#FFFFFF"]},
{"material":"PLA","type":"Support","name":"Support for PLA/PETG Black","code":"65103","colours":["#000000"]},
{"material":"PLA","type":"Support","name":"Support for PLA/PETG Nature","code":"65102","colours":["#FBF5E4"]},
{"material":"PLA","type":"Tough","name":"Black","code":"","colours":["#000000"]},
{"material":"PLA","type":"Tough","name":"Cream White","code":"","colours":["#FAFAFA"]},
{"material":"PLA","type":"Tough","name":"Gray","code":"12102","colours":["#515A6C"]},
{"material":"PLA","type":"Tough","name":"Lavender-Blue","code":"12700","colours":["#6667AB"]},
{"material":"PLA","type":"Tough","name":"Light-Blue","code":"12600","colours":["#0085AD"]},
{"material":"PLA","type":"Tough","name":"Orange","code":"12300","colours":["#FF7F41"]},
{"material":"PLA","type":"Tough","name":"Pine Green","code":"12500","colours":["#00482B"]},
{"material":"PLA","type":"Tough","name":"Silver","code":"12103","colours":["#9D9D9D"]},
{"material":"PLA","type":"Tough","name":"Vermilion-Red","code":"12200","colours":["#DD3C22"]},
{"material":"PLA","type":"Tough","name":"White","code":"12100","colours":["#FFFFFF"]},
{"material":"PLA","type":"Tough","name":"Yellow","code":"12400","colours":["#FEDB00"]},
{"material":"PLA","type":"Tough+","name":"Black","code":"12104","colours":["#000000"]},
{"material":"PLA","type":"Tough+","name":"Cyan","code":"12601","colours":["#009BD9"]},
{"material":"PLA","type":"Tough+","name":"Gray","code":"12105","colours":["#AFB1AE"]},
{"material":"PLA","type":"Tough+","name":"Orange","code":"12301","colours":["#DC3A27"]},
{"material":"PLA","type":"Tough+","name":"Silver","code":"12106","colours":["#959698"]},
{"material":"PLA","type":"Tough+","name":"White","code":"12107","colours":["#FFFFFF"]},
{"material":"PLA","type":"Tough+","name":"Yellow","code":"12401","colours":["#F4D53F"]},
{"material":"PLA","type":"Translucent","name":"Blue","code":"13611","colours":["#0047BB"]},
{"material":"PLA","type":"Translucent","name":"Cherry Pink","code":"13211","colours":["#F5B6CD"]},
{"material":"PLA","type":"Translucent","name":"Ice Blue","code":"13610","colours":["#B8CDE9"]},
{"material":"PLA","type":"Translucent","name":"Lavender","code":"13711","colours":["#B8ACD6"]},
{"material":"PLA","type":"Translucent","name":"Light Jade","code":"13510","colours":["#96D8AF"]},
{"material":"PLA","type":"Translucent","name":"Mellow Yellow","code":"13410","colours":["#F5DBAB"]},
{"material":"PLA","type":"Translucent","name":"Orange","code":"13301","colours":["#F74E02"]},
{"material":"PLA","type":"Translucent","name":"Purple","code":"13710","colours":["#8344B0"]},
{"material":"PLA","type":"Translucent","name":"Red","code":"13210","colours":["#B50011"]},
{"material":"PLA","type":"Translucent","name":"Teal","code":"13612","colours":["#009FA1"]},
{"material":"PLA","type":"Wood","name":"Black Walnut","code":"13107","colours":["#4F3F24"]},
{"material":"PLA","type":"Wood","name":"Classic Birch","code":"13505","colours":["#918669"]},
{"material":"PLA","type":"Wood","name":"Clay Brown","code":"13801","colours":["#995F11"]},
{"material":"PLA","type":"Wood","name":"Ochre Yellow","code":"13403","colours":["#C98935"]},
{"material":"PLA","type":"Wood","name":"Rosewood","code":"13204","colours":["#4C241C"]},
{"material":"PLA","type":"Wood","name":"White Oak","code":"13106","colours":["#D6CCA3"]},
{"material":"PPA","type":"CF","name":"Black","code":"73100","colours":["#000000"]},
{"material":"PPS","type":"CF","name":"Black","code":"76100","colours":["#000000"]},
{"material":"PVA","type":"Support","name":"Clear","code":"66400","colours":["#F0F1A8"]},
{"material":"TPU","type":"68D (For AMS)","name":"Black","code":"53101","colours":["#000000"]},
{"material":"TPU","type":"68D (For AMS)","name":"Blue","code":"53600","colours":["#5898DD"]},
{"material":"TPU","type":"68D (For AMS)","name":"Gray","code":"53102","colours":["#939393"]},
{"material":"TPU","type":"68D (For AMS)","name":"Neon Green","code":"53500","colours":["#90FF1A"]},
{"material":"TPU","type":"68D (For AMS)","name":"Red","code":"53200","colours":["#ED0000"]},
{"material":"TPU","type":"68D (For AMS)","name":"White","code":"53100","colours":["#FFFFFF"]},
{"material":"TPU","type":"68D (For AMS)","name":"Yellow","code":"53400","colours":["#F9EF41"]},
{"material":"TPU","type":"85A","name":"Black","code":"51107","colours":["#000000"]},
{"material":"TPU","type":"85A","name":"Flesh","code":"51201","colours":["#FFF2D5"]},
{"material":"TPU","type":"85A","name":"Light Cyan","code":"51500","colours":["#C3E2D6"]},
{"material":"TPU","type":"85A","name":"Lime Green","code":"51501","colours":["#EAF28F"]},
{"material":"TPU","type":"85A","name":"Neon Orange","code":"51305","colours":["#F68B1B"]},
{"material":"TPU","type":"90A","name":"Black","code":"51103","colours":["#000000"]},
{"material":"TPU","type":"90A","name":"Blaze","code":"51901","colours":["#F1AAA8","#D21B3C"]},
{"material":"TPU","type":"90A","name":"Cocoa Brown","code":"51800","colours":["#5C4738"]},
{"material":"TPU","type":"90A","name":"Crystal Blue","code":"51601","colours":["#7EB4E1"]},
{"material":"TPU","type":"90A","name":"Frozen","code":"51900","colours":["#FFFFFF","#40B6E4"]},
{"material":"TPU","type":"90A","name":"Grape Jelly","code":"51700","colours":["#D6ABFF"]},
{"material":"TPU","type":"90A","name":"Quicksilver","code":"51106","colours":["#9EA2A2"]},
{"material":"TPU","type":"90A","name":"White","code":"51105","colours":["#FFFFFF"]},
{"material":"TPU","type":"95A HF/High Speed","name":"Black","code":"51100","colours":["#000000"]},
{"material":"TPU","type":"95A HF/High Speed","name":"Blue","code":"51600","colours":["#0072CE"]},
{"material":"TPU","type":"95A HF/High Speed","name":"Gray","code":"51101","colours":["#898D8D"]},
{"material":"TPU","type":"95A HF/High Speed","name":"Red","code":"51200","colours":["#C8102E"]},
{"material":"TPU","type":"95A HF/High Speed","name":"White","code":"51102","colours":["#FFFFFF"]},
{"material":"TPU","type":"95A HF/High Speed","name":"Yellow","code":"51400","colours":["#F3E600"]}
]
}

View file

@ -277,7 +277,6 @@ CREATE TABLE IF NOT EXISTS filament_spools (
total_g INTEGER, -- spool weight when new
remaining_g INTEGER, -- what's left; null = unknown
ams_slot TEXT, -- "<printer>/<unit>/<slot>" once AMS sync lands
code TEXT, -- maker's filament code (Bambu 10101 etc.), what the AMS reports
notes TEXT NOT NULL DEFAULT '',
date_added TIMESTAMPTZ NOT NULL DEFAULT now(),
date_modified TIMESTAMPTZ
@ -288,4 +287,3 @@ CREATE TABLE IF NOT EXISTS filament_spools (
-- thumbnails and fingerprints are done, because the reader shells out to Python.
ALTER TABLE models ADD COLUMN IF NOT EXISTS filaments JSONB;
ALTER TABLE models ADD COLUMN IF NOT EXISTS filaments_status TEXT; -- pending | done | none | error
ALTER TABLE filament_spools ADD COLUMN IF NOT EXISTS code TEXT;

View file

@ -25,89 +25,22 @@ const PENDING = `
// "PLA Basic" and "pla basic" are the same filament; "PLA-CF" is not.
const normalise = (s) => String(s || '').trim().toUpperCase().replace(/\s+/g, ' ');
// Bambu writes #RRGGBB and #RRGGBBAA; the alpha channel is not a colour.
function rgb(hex) {
const m = /^#?([0-9a-f]{6})(?:[0-9a-f]{2})?$/i.exec(String(hex || '').trim());
if (!m) return null;
const n = parseInt(m[1], 16);
return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 };
}
// Redmean distance: closer to how an eye judges two colours than plain RGB.
// 0 is identical, black to white is about 765. Null when either is unknown.
function colourDistance(a, b) {
const x = rgb(a);
const y = rgb(b);
if (!x || !y) return null;
const rm = (x.r + y.r) / 2;
const dr = x.r - y.r;
const dg = x.g - y.g;
const db = x.b - y.b;
return Math.sqrt((2 + rm / 256) * dr * dr + 4 * dg * dg + (2 + (255 - rm) / 256) * db * db);
}
// How close counts as the same colour. 50 keeps #000000 and #161616 apart (~66)
// while allowing genuinely near shades to stand in for each other.
const COLOUR_TOLERANCE = 50;
// Which spool fills which slot. One spool cannot fill two slots, so the best
// colour matches are assigned first. A slot whose project gives no colour is
// satisfied by any spool of the right material.
function assignSpools(filaments, spools) {
const slots = (filaments || []).map((f, i) => ({
index: i, material: normalise(f.type), colour: f.colour || null,
}));
const shelf = (spools || [])
// The materials a model needs that nothing on hand provides. Colour is ignored
// on purpose: matching a project's exact colours would make almost nothing
// printable, and a different colour still prints.
function missingMaterials(filaments, spools) {
const onHand = new Set(spools
.filter((s) => s.remaining_g === null || s.remaining_g === undefined || Number(s.remaining_g) > 0)
.map((s, i) => ({
key: s.id == null ? `i${i}` : `s${s.id}`, material: normalise(s.material),
colour: s.colour_hex || null, name: s.colour || s.material,
}));
const pairs = [];
for (const slot of slots) {
for (const spool of shelf) {
if (spool.material !== slot.material) continue;
if (slot.colour === null) { pairs.push({ slot: slot.index, spool: spool.key, d: 0 }); continue; }
const d = colourDistance(slot.colour, spool.colour);
if (d !== null && d <= COLOUR_TOLERANCE) pairs.push({ slot: slot.index, spool: spool.key, d });
}
}
pairs.sort((a, b) => a.d - b.d);
const filledBy = new Map();
const usedSpools = new Set();
for (const p of pairs) {
if (filledBy.has(p.slot) || usedSpools.has(p.spool)) continue;
filledBy.set(p.slot, p.spool);
usedSpools.add(p.spool);
}
return slots.map((slot) => {
const key = filledBy.get(slot.index);
const by = key ? shelf.find((s) => s.key === key) : null;
let nearest = null;
if (!by && slot.colour) {
for (const spool of shelf) {
if (spool.material !== slot.material) continue;
const d = colourDistance(slot.colour, spool.colour);
if (d !== null && (!nearest || d < nearest.distance)) nearest = { name: spool.name, distance: Math.round(d) };
}
}
return { ...slot, filled: Boolean(by), by: by ? by.name : null, nearest };
});
.map((s) => normalise(s.material)));
const needed = [...new Set((filaments || []).map((f) => normalise(f.type)).filter(Boolean))];
return needed.filter((type) => !onHand.has(type));
}
// The slots you can't fill from the shelf.
function shortfall(filaments, spools) {
return assignSpools(filaments, spools).filter((s) => !s.filled);
}
// True when every slot has a spool. A model we know nothing about is not
// claimed to be printable.
// True when every filament the model names is on a spool you still have.
// A model we know nothing about is not claimed to be printable.
function isPrintable(filaments, spools) {
if (!filaments || !filaments.length) return false;
return shortfall(filaments, spools).length === 0;
return missingMaterials(filaments, spools).length === 0;
}
// One pass. Returns whether it did any work, so it fits the background worker
@ -149,7 +82,4 @@ async function reread() {
await db.query('UPDATE models SET filaments_status = NULL');
}
module.exports = {
run, pendingCount, reread,
assignSpools, shortfall, isPrintable, normalise, colourDistance, rgb, COLOUR_TOLERANCE,
};
module.exports = { run, pendingCount, reread, missingMaterials, isPrintable, normalise };

View file

@ -665,10 +665,10 @@ async function listSpools() {
async function createSpool(data) {
const { rows } = await db.query(
`INSERT INTO filament_spools (material, colour, colour_hex, brand, total_g, remaining_g, notes, code)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING *`,
`INSERT INTO filament_spools (material, colour, colour_hex, brand, total_g, remaining_g, notes)
VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING *`,
[String(data.material || '').trim(), data.colour || '', data.colour_hex || null, data.brand || '',
data.total_g ?? null, data.remaining_g ?? null, data.notes || '', data.code || null]);
data.total_g ?? null, data.remaining_g ?? null, data.notes || '']);
return rows[0];
}
@ -683,11 +683,10 @@ async function updateSpool(id, data) {
total_g = COALESCE($6, total_g),
remaining_g = COALESCE($7, remaining_g),
notes = COALESCE($8, notes),
code = COALESCE($9, code),
date_modified = now()
WHERE id = $1 RETURNING *`,
[id, data.material ?? null, data.colour ?? null, data.colour_hex ?? null, data.brand ?? null,
data.total_g ?? null, data.remaining_g ?? null, data.notes ?? null, data.code ?? null]);
data.total_g ?? null, data.remaining_g ?? null, data.notes ?? null]);
return rows[0] || null;
}

View file

@ -6,7 +6,6 @@ let config = { importers: {} };
let stats = { trash: 0, duplicateGroups: 0, fingerprintsPending: 0 };
let selectedScope = 'all'; // 'all' | 'duplicates' | 'trash' | a collection id
let spools = []; // filament on hand, so cards can say what's printable
let palette = []; // the maker's filament list: names, codes and colours
let tagFilters = {}; // tag -> 'include' | 'exclude'
let duplicateData = null; // last /api/duplicates response
let duplicatePoll = null;
@ -160,11 +159,6 @@ async function loadData() {
config = await configRes.json();
stats = statsRes.ok ? await statsRes.json() : stats;
spools = filamentRes.ok ? (await filamentRes.json()).spools : [];
// Static and ~40KB, so fetched once rather than on every refresh.
if (!palette.length) {
palette = await fetch('/api/filament/palette')
.then((r) => (r.ok ? r.json() : { filaments: [] })).then((p) => p.filaments || []).catch(() => []);
}
if (selectedScope === 'trash') await loadTrash();
if (!SPECIAL_SCOPES.includes(selectedScope) && !projects.some((p) => String(p.id) === String(selectedScope))) {
selectedScope = 'all'; // the open collection was deleted
@ -271,96 +265,22 @@ const licenseKey = (m) => (m.commercial_use === true ? 'sellable' : m.commercial
// rather than "no" — only slicer projects name their filaments.
const filamentType = (s) => String(s || '').trim().toUpperCase().replace(/\s+/g, ' ');
// Mirrors lib/filaments.js. Bambu writes #RRGGBB and #RRGGBBAA; alpha isn't colour.
function rgbOf(hex) {
const m = /^#?([0-9a-f]{6})(?:[0-9a-f]{2})?$/i.exec(String(hex || '').trim());
if (!m) return null;
const n = parseInt(m[1], 16);
return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 };
}
// Redmean distance: nearer to how an eye judges colours than plain RGB.
function colourDistance(a, b) {
const x = rgbOf(a);
const y = rgbOf(b);
if (!x || !y) return null;
const rm = (x.r + y.r) / 2;
const dr = x.r - y.r;
const dg = x.g - y.g;
const db = x.b - y.b;
return Math.sqrt((2 + rm / 256) * dr * dr + 4 * dg * dg + (2 + (255 - rm) / 256) * db * db);
}
const COLOUR_TOLERANCE = 50; // keeps #000000 and #161616 (~66 apart) distinct
// The maker's name for a colour, when the palette has one that close. The
// threshold is deliberately tight: this is identification, not substitution.
function paletteName(hex, material) {
let best = null;
for (const f of palette) {
if (material && filamentType(f.material) !== filamentType(material)) continue;
for (const c of f.colours || []) {
const d = colourDistance(hex, c);
if (d !== null && d <= 12 && (!best || d < best.d)) best = { d, name: f.name, type: f.type };
}
}
return best ? `${best.type} ${best.name}` : null;
}
// Which spool fills which slot — best colour matches first, one spool per slot.
function printability(m) {
const slots = ((m && m.filaments) || []).map((f, i) => ({
index: i, material: filamentType(f.type), colour: f.colour || null,
}));
if (!slots.length) return { known: false, slots: [], unfilled: [], ok: false };
const shelf = spools
const needed = [...new Set(((m && m.filaments) || []).map((f) => filamentType(f.type)).filter(Boolean))];
if (!needed.length) return { known: false, needed: [], missing: [], ok: false };
const onHand = new Set(spools
.filter((s) => s.remaining_g == null || Number(s.remaining_g) > 0)
.map((s, i) => ({ key: s.id == null ? `i${i}` : `s${s.id}`, material: filamentType(s.material),
colour: s.colour_hex || null, name: s.colour || s.material }));
const pairs = [];
for (const slot of slots) {
for (const spool of shelf) {
if (spool.material !== slot.material) continue;
if (slot.colour === null) { pairs.push({ slot: slot.index, spool: spool.key, d: 0 }); continue; }
const d = colourDistance(slot.colour, spool.colour);
if (d !== null && d <= COLOUR_TOLERANCE) pairs.push({ slot: slot.index, spool: spool.key, d });
}
}
pairs.sort((a, b) => a.d - b.d);
const filledBy = new Map();
const used = new Set();
for (const p of pairs) {
if (filledBy.has(p.slot) || used.has(p.spool)) continue;
filledBy.set(p.slot, p.spool);
used.add(p.spool);
}
const resolved = slots.map((slot) => {
const key = filledBy.get(slot.index);
const by = key ? shelf.find((s) => s.key === key) : null;
let nearest = null;
if (!by && slot.colour) {
for (const spool of shelf) {
if (spool.material !== slot.material) continue;
const d = colourDistance(slot.colour, spool.colour);
if (d !== null && (!nearest || d < nearest.distance)) nearest = { name: spool.name, distance: Math.round(d) };
}
}
return { ...slot, filled: Boolean(by), by: by ? by.name : null, nearest };
});
const unfilled = resolved.filter((s) => !s.filled);
return { known: true, slots: resolved, unfilled, ok: unfilled.length === 0 };
.map((s) => filamentType(s.material)));
const missing = needed.filter((t) => !onHand.has(t));
return { known: true, needed, missing, ok: missing.length === 0 };
}
function printBadgeHtml(m) {
const p = printability(m);
if (!p.known) return '';
if (p.ok) return `<span class="print-badge ok" title="A spool for every slot">✔ printable</span>`;
const n = p.unfilled.length;
return `<span class="print-badge no" title="${n} of ${p.slots.length} slots have no matching spool">needs ${n} colour${n === 1 ? '' : 's'}</span>`;
return p.ok
? '<span class="print-badge ok" title="Every filament it needs is on a spool">✔ printable</span>'
: `<span class="print-badge no" title="Not on the shelf: ${escapeHtml(p.missing.join(', '))}">needs ${escapeHtml(p.missing.join(', '))}</span>`;
}
const totalSize = (m) => (m.files || []).reduce((s, f) => s + Number(f.size || 0), 0);
const fileCountLabel = (m) => `${m.files.length} file${m.files.length === 1 ? '' : 's'}`;
@ -2141,24 +2061,12 @@ function renderDetailPrintable(model) {
el.innerHTML = '<div class="lic-note">This model doesn’t name its filaments — only Bambu Studio and OrcaSlicer projects do.</div>';
return;
}
const rows = p.slots.map((s, i) => `
<div class="slot-row">
<span class="spool-dot" style="background:${escapeHtml(s.colour || '#888888')}"></span>
<span>Slot ${i + 1} · ${escapeHtml(s.material)}</span>
<span class="form-hint">${escapeHtml(s.colour || 'any colour')}${(() => {
const named = s.colour ? paletteName(s.colour, s.material) : null;
return named ? ` · ${escapeHtml(named)}` : '';
})()}</span>
${s.filled
? `<span class="print-badge ok">${escapeHtml(s.by)}</span>`
: `<span class="print-badge no">none</span>${s.nearest
? `<span class="form-hint">nearest: ${escapeHtml(s.nearest.name)}</span>` : ''}`}
</div>`).join('');
el.innerHTML = `<div>${p.ok
? '<span class="print-badge ok">✔ printable now</span>'
: `<span class="print-badge no">needs ${p.unfilled.length} colour${p.unfilled.length === 1 ? '' : 's'}</span>`}</div>
<div class="slot-list">${rows}</div>
<div class="lic-note">A spool fills one slot, and its colour has to be close to what the slot asks for.</div>`;
el.innerHTML = p.ok
? `<div><span class="print-badge ok">✔ printable now</span></div>
<div class="lic-note">Needs ${escapeHtml(p.needed.join(', '))} — all on the shelf.</div>`
: `<div><span class="print-badge no">not right now</span></div>
<div class="lic-note">Needs ${escapeHtml(p.needed.join(', '))}; you have no ${escapeHtml(p.missing.join(', '))}.
Colour isn’t matched — a different colour still prints.</div>`;
}
function renderDetailLicense(model) {
@ -2339,12 +2247,9 @@ function filamentPanelHtml() {
know which filaments they ask for, so Printhive can tell you what's printable without a trip to the shelf.
Colour isn't matched — a different colour still prints.</p>
<form id="spoolForm" class="watch-form">
<input type="text" id="spoolPick" list="spoolPickList" placeholder="Pick a Bambu filament…"
aria-label="Pick a filament from the maker's list" autocomplete="off">
<datalist id="spoolPickList"></datalist>
<input type="text" id="spoolMaterial" placeholder="Material (PLA, PETG…)" aria-label="Material" required>
<input type="text" id="spoolColour" placeholder="Colour name" aria-label="Colour">
<input type="color" id="spoolHex" value="#ffffff" aria-label="Colour swatch" title="The spool's colour — matched against what each slot asks for">
<input type="color" id="spoolHex" value="#22c55e" aria-label="Colour swatch" title="Colour">
<input type="text" id="spoolBrand" placeholder="Brand" aria-label="Brand">
<input type="number" id="spoolRemaining" placeholder="Grams left" aria-label="Grams left" min="0" step="10">
<button type="submit" class="btn btn-primary" id="spoolSubmit">Add spool</button>
@ -2381,20 +2286,6 @@ async function renderFilamentPanel() {
if (!document.getElementById('spoolForm')) {
fileGrid.innerHTML = filamentPanelHtml();
document.getElementById('spoolForm').addEventListener('submit', handleAddSpool);
// Picking a known filament fills in the rest, so nobody types hex by hand.
const pick = document.getElementById('spoolPick');
document.getElementById('spoolPickList').innerHTML = palette
.map((f) => `<option value="${escapeHtml(`${f.material} ${f.type} — ${f.name}${f.code ? ` (${f.code})` : ''}`)}"></option>`)
.join('');
pick.addEventListener('input', () => {
const chosen = palette.find((f) => `${f.material} ${f.type} — ${f.name}${f.code ? ` (${f.code})` : ''}` === pick.value);
if (!chosen) return;
document.getElementById('spoolMaterial').value = chosen.material;
document.getElementById('spoolColour').value = chosen.name;
document.getElementById('spoolHex').value = (chosen.colours[0] || '#ffffff').toLowerCase();
pick.dataset.code = chosen.code || '';
});
}
await refreshSpools();
}
@ -2437,14 +2328,12 @@ async function handleAddSpool(e) {
status.textContent = '';
try {
const remaining = document.getElementById('spoolRemaining').value;
const pick = document.getElementById('spoolPick');
await postJson('/api/filament', {
material,
colour: document.getElementById('spoolColour').value.trim(),
colour_hex: document.getElementById('spoolHex').value,
brand: document.getElementById('spoolBrand').value.trim(),
remaining_g: remaining === '' ? null : Number(remaining),
code: (pick && pick.dataset.code) || null, // what the AMS reports, when picked from the list
});
document.getElementById('spoolForm').reset();
document.getElementById('spoolMaterial').focus();

View file

@ -464,8 +464,6 @@ label { display: block; font-size: 13px; font-weight: 500; color: var(--text-mut
.watch-head { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; font-size: 14px; }
.watch-actions { margin-left: auto; display: flex; flex-wrap: wrap; gap: 8px; }
.watch-actions .btn-sm { align-self: auto; }
.slot-list { display: flex; flex-direction: column; gap: 6px; margin: 8px 0; }
.slot-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; font-size: 13px; }
.print-badge { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 11px; white-space: nowrap; }
.print-badge.ok { background: color-mix(in srgb, #22c55e 18%, transparent); color: #4ade80; }
.print-badge.no { background: color-mix(in srgb, #f59e0b 16%, transparent); color: #fbbf24; }

View file

@ -29,7 +29,6 @@ const aiRuns = require('./lib/aiRuns');
const collectionWatch = require('./lib/collectionWatch');
const autotag = require('./lib/autotag');
const filaments = require('./lib/filaments');
const filamentPalette = require('./db/bambu-filaments.json');
const embeddings = require('./lib/embeddings');
const app = express();
@ -324,13 +323,6 @@ app.put('/api/settings', async (req, res, next) => {
// ----------------------------------------------------------------- filament
// The spools you have. What a model needs is read from its 3MF in the
// background and kept on the model itself, so matching is a plain comparison.
// The maker's filament list, for picking a spool without typing hex codes.
// Static, so it's cached hard rather than re-read.
app.get('/api/filament/palette', (req, res) => {
res.set('Cache-Control', 'public, max-age=86400');
res.json(filamentPalette);
});
app.get('/api/filament', async (req, res, next) => {
try {
res.json({ spools: await store.listSpools(), pending: await filaments.pendingCount() });

View file

@ -2,85 +2,45 @@ const test = require('node:test');
const assert = require('node:assert/strict');
const filaments = require('../lib/filaments');
const spool = (material, hex, name) => ({ id: hex, material, colour_hex: hex, colour: name || hex, remaining_g: 500 });
const spools = (...materials) => materials.map((m, i) => ({ id: i + 1, material: m, remaining_g: 500 }));
// The model that started this: four PLA slots, one spool of Jade White.
const pooh = [
{ type: 'PLA', colour: '#000000' },
{ type: 'PLA', colour: '#586579' },
{ type: 'PLA', colour: '#8E9089' },
{ type: 'PLA', colour: '#FFFFFF' },
];
test('a four-colour print needs those colours, not four spools of anything', () => {
const shelf = [spool('PLA', '#ffffff', 'Jade White')];
const slots = filaments.assignSpools(pooh, shelf);
assert.deepEqual(slots.map((s) => s.filled), [false, false, false, true]);
assert.equal(slots[3].by, 'Jade White');
assert.equal(filaments.isPrintable(pooh, shelf), false);
assert.equal(filaments.shortfall(pooh, shelf).length, 3);
// Four white spools still can't print it: the other three colours are missing.
const fourWhite = ['a', 'b', 'c', 'd'].map((k) => ({ ...spool('PLA', '#ffffff', 'Jade White'), id: k }));
assert.equal(filaments.isPrintable(pooh, fourWhite), false);
test('a model is printable when every filament it names is on a spool', () => {
const project = [{ type: 'PLA', colour: '#00AE42' }, { type: 'PETG', colour: '#000000' }];
assert.equal(filaments.isPrintable(project, spools('PLA', 'PETG', 'ABS')), true);
assert.deepEqual(filaments.missingMaterials(project, spools('PLA', 'PETG')), []);
});
test('with every colour on the shelf it prints', () => {
const shelf = [spool('PLA', '#000000', 'Black'), spool('PLA', '#586579', 'Blue Grey'),
spool('PLA', '#8E9089', 'Grey'), spool('PLA', '#ffffff', 'Jade White')];
assert.equal(filaments.isPrintable(pooh, shelf), true);
assert.deepEqual(filaments.assignSpools(pooh, shelf).map((s) => s.by),
['Black', 'Blue Grey', 'Grey', 'Jade White']);
test('what is missing is named, not just refused', () => {
const project = [{ type: 'PLA' }, { type: 'PETG' }, { type: 'TPU' }];
assert.deepEqual(filaments.missingMaterials(project, spools('PLA')), ['PETG', 'TPU']);
assert.equal(filaments.isPrintable(project, spools('PLA')), false);
});
test('one spool cannot fill two slots of the same colour', () => {
const twoBlack = [{ type: 'PLA', colour: '#000000' }, { type: 'PLA', colour: '#000000' }];
assert.equal(filaments.isPrintable(twoBlack, [spool('PLA', '#000000', 'Black')]), false);
assert.equal(filaments.isPrintable(twoBlack,
[spool('PLA', '#000000', 'Black'), { ...spool('PLA', '#000000', 'Black 2'), id: 'b2' }]), true);
test('colour is ignored — a different colour still prints', () => {
const project = [{ type: 'PLA', colour: '#FF0000' }];
assert.equal(filaments.isPrintable(project, [{ material: 'PLA', colour_hex: '#0000FF', remaining_g: 200 }]), true);
});
test('8-digit hex with alpha parses as its colour', () => {
assert.deepEqual(filaments.rgb('#FFFFFFFF'), { r: 255, g: 255, b: 255 });
assert.deepEqual(filaments.rgb('#586579'), { r: 88, g: 101, b: 121 });
assert.equal(filaments.rgb('not a colour'), null);
// A slot written with alpha still matches a plain spool of that colour.
assert.equal(filaments.isPrintable([{ type: 'PLA', colour: '#FFFFFFFF' }],
[spool('PLA', '#ffffff', 'Jade White')]), true);
});
test('near shades count, but near-black is not black', () => {
const nearWhite = [{ type: 'PLA', colour: '#FDFDFD' }];
assert.equal(filaments.isPrintable(nearWhite, [spool('PLA', '#ffffff', 'Jade White')]), true);
// #161616 sits about 66 away from black, beyond the tolerance of 50.
assert.ok(filaments.colourDistance('#000000', '#161616') > filaments.COLOUR_TOLERANCE);
assert.equal(filaments.isPrintable([{ type: 'PLA', colour: '#161616' }],
[spool('PLA', '#000000', 'Black')]), false);
});
test('the nearest spool is named when a slot can’t be filled', () => {
const [slot] = filaments.assignSpools([{ type: 'PLA', colour: '#000000' }],
[spool('PLA', '#ffffff', 'Jade White')]);
assert.equal(slot.filled, false);
assert.equal(slot.nearest.name, 'Jade White');
assert.ok(slot.nearest.distance > 0);
});
test('material still matters, and a slot with no colour takes any spool of it', () => {
assert.equal(filaments.isPrintable([{ type: 'PETG', colour: '#ffffff' }],
[spool('PLA', '#ffffff', 'Jade White')]), false);
assert.equal(filaments.isPrintable([{ type: 'PLA', colour: null }],
[spool('PLA', '#123456', 'Anything')]), true);
test('case and spacing don’t matter, but a different material does', () => {
assert.equal(filaments.normalise(' pla basic '), 'PLA BASIC');
// PLA-CF is carbon filled: a plain PLA spool is not a substitute.
assert.deepEqual(filaments.missingMaterials([{ type: 'PLA-CF' }], spools('PLA')), ['PLA-CF']);
assert.deepEqual(filaments.missingMaterials([{ type: 'pla' }], spools('PLA')), []);
});
test('an empty spool doesn’t count, but an unweighed one does', () => {
const one = [{ type: 'PLA', colour: '#ffffff' }];
assert.equal(filaments.isPrintable(one, [{ ...spool('PLA', '#ffffff', 'Jade White'), remaining_g: 0 }]), false);
assert.equal(filaments.isPrintable(one, [{ ...spool('PLA', '#ffffff', 'Jade White'), remaining_g: null }]), true);
const project = [{ type: 'PLA' }];
assert.deepEqual(filaments.missingMaterials(project, [{ material: 'PLA', remaining_g: 0 }]), ['PLA']);
assert.deepEqual(filaments.missingMaterials(project, [{ material: 'PLA', remaining_g: null }]), []);
assert.deepEqual(filaments.missingMaterials(project, [{ material: 'PLA' }]), []);
});
test('a model we know nothing about is not claimed to be printable', () => {
assert.equal(filaments.isPrintable(null, [spool('PLA', '#ffffff')]), false);
assert.equal(filaments.isPrintable([], [spool('PLA', '#ffffff')]), false);
assert.deepEqual(filaments.assignSpools(null, []), []);
assert.equal(filaments.isPrintable(null, spools('PLA')), false);
assert.equal(filaments.isPrintable([], spools('PLA')), false);
});
test('the same material twice is asked for once', () => {
const project = [{ type: 'PLA', colour: '#FFF' }, { type: 'PLA', colour: '#000' }];
assert.deepEqual(filaments.missingMaterials(project, []), ['PLA']);
});