diff --git a/web/themes/default/app.js b/web/themes/default/app.js index ebd7204..fc01ae5 100644 --- a/web/themes/default/app.js +++ b/web/themes/default/app.js @@ -2926,29 +2926,85 @@ function openFilamentDialog(slots){ var dr=ar[0]-br[0], dg=ar[1]-br[1], db=ar[2]-br[2]; return (dr*dr + dg*dg + db*db); } - var defaultSlotByPaint={}; - var usedDefaultSlot={}; - channels.forEach(function(gc,i){ + + // Globale, farboptimale Zuordnung Kanal -> Slot. + // Anforderungen: 1) Material muss passen (harter Filter, wie bisher) + // 2) Farbe so nah wie möglich + // 3) Position spielt praktisch keine Rolle mehr + // Ein rein gieriger Ansatz (Kanal für Kanal den "nächsten" Slot wählen) + // kann bei knappen/mehrdeutigen Slots suboptimale Gesamtlösungen liefern + // (z.B. Kanal A "verbraucht" den Slot, der eigentlich viel besser zu + // Kanal B gepasst hätte). Da ein AMS in der Praxis nur wenige Slots hat + // (üblich: bis zu 4), lohnt sich Brute-Force über alle möglichen + // Zuordnungen mit Pruning - liefert garantiert das global beste Ergebnis, + // ganz ohne einen vollen Hungarian-Algorithmus implementieren zu müssen. + function _bestAssignment(slotsPerChannel){ + var n=slotsPerChannel.length; + var assign=new Array(n).fill(-1); + + // Sicherheitsnetz für unrealistisch viele Kanäle: gieriger Fallback + // (kleinster Farbabstand zuerst bedient), um exponentielle Laufzeit zu + // vermeiden. Greift in der Praxis nie, ein AMS hat max. wenige Slots. + if(n>8){ + var used={}; + var order=slotsPerChannel.map(function(_,idx){return idx;}).sort(function(a,b){ + var da=slotsPerChannel[a].length?slotsPerChannel[a][0].dist:Infinity; + var db=slotsPerChannel[b].length?slotsPerChannel[b][0].dist:Infinity; + return da-db; + }); + order.forEach(function(i){ + var cands=slotsPerChannel[i]; + if(!cands.length)return; + var pick=cands.find(function(c){return !used[c.slot_index];})||cands[0]; + assign[i]=pick.slot_index; + used[pick.slot_index]=true; + }); + return assign; + } + + var bestCost=Infinity, bestAssign=assign.slice(), used={}; + (function backtrack(idx,cost){ + if(cost>=bestCost)return; // Pruning: kann eh nicht mehr besser werden + if(idx===n){ bestCost=cost; bestAssign=assign.slice(); return; } + var candidates=slotsPerChannel[idx]; + if(!candidates.length){ + assign[idx]=-1; + backtrack(idx+1,cost); + return; + } + var placedFree=false; + for(var k=0;k Mehrfachbelegung des + // farblich besten kompatiblen Slots zulassen (besser als leer) + var best=candidates[0]; + assign[idx]=best.slot_index; + backtrack(idx+1,cost+best.dist); + } + assign[idx]=-1; + })(0,0); + return bestAssign; + } + + var slotsPerChannel=channels.map(function(gc){ var compatible=_amsSlots.filter(function(s){ return _materialsCompatible(gc.material, s.material); }); - if(!compatible.length){ - defaultSlotByPaint[i]=-1; - return; - } - - var ranked=compatible.slice().sort(function(a,b){ - var da=Math.abs((a.slot_index||0)-i), db=Math.abs((b.slot_index||0)-i); - if(da!==db)return da-db; - var ca=_colorDist(gc.color_hex, a.color_hex), cb=_colorDist(gc.color_hex, b.color_hex); - if(ca!==cb)return ca-cb; - return (a.slot_index||0)-(b.slot_index||0); - }); - - var chosen=ranked.find(function(s){return !usedDefaultSlot[s.slot_index];}) || ranked[0]; - defaultSlotByPaint[i]=chosen?chosen.slot_index:-1; - if(chosen) usedDefaultSlot[chosen.slot_index]=1; + return compatible.map(function(s){ + return {slot_index:s.slot_index, dist:_colorDist(gc.color_hex, s.color_hex)}; + }).sort(function(a,b){ return a.dist-b.dist; }); }); + var _assignResult=_bestAssignment(slotsPerChannel); + var defaultSlotByPaint={}; + channels.forEach(function(gc,i){ defaultSlotByPaint[i]=_assignResult[i]; }); if(!_amsSlots.length){ body.innerHTML='

'+T.fd_no_slots_msg.replace('{br}','
')+'

'; @@ -3402,4 +3458,4 @@ function loadPrinterTab(){ }).catch(function(e){ if(grid)grid.innerHTML='
Fehler: '+e+'
'; }); -} +} \ No newline at end of file