feat(nav): colour crates by type in the rack diagram (material_color)

/nav/rack returns each crate's virtual_crate_type.material_color + type name;
drawRack fills/strokes/arrows each crate in its real colour (Blue Crate #0000FF,
Wooden Rack Bin #BA906A, White Tub #FFFFFF, Black Tub #191970, Shelf #C0C0C0…)
instead of all-blue. Legend lists the crate types present on the rack.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-06-23 11:26:34 +10:00
parent cba339151f
commit 13e5200b8d
2 changed files with 13 additions and 6 deletions

View File

@ -67,8 +67,9 @@ async def rack_detail(rack_id: int, ident=Depends(require_token), db=Depends(get
crates = [dict(r) for r in (await db.execute(text("""
SELECT c.id, c.name, c.label_text, c.rack_level_index::int AS level, c.slot_number::int AS slot,
c.direction, c.pos_x::float AS x, c.pos_z::float AS z, c.crate_purpose,
ct.name AS crate_type, ct.material_color AS color,
(SELECT count(*) FROM inventory i WHERE i.crate_id = c.id AND i.in_stock) AS items
FROM virtual_crate c
FROM virtual_crate c LEFT JOIN virtual_crate_type ct ON ct.id = c.crate_type_id
WHERE c.rack_id = :id AND c.visible = 'y'
ORDER BY c.rack_level_index NULLS FIRST, c.slot_number NULLS LAST
"""), {"id": rack_id})).mappings()]

View File

@ -145,6 +145,10 @@ const money=n=>n==null?'—':'$'+Number(n).toFixed(2);
const esc=s=>(s||'').replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
const imgTag=it=> it.release_id ? `<img src="/img/r/${it.release_id}" onerror="this.onerror=null;this.src='${(it.thumb||'').replace(/'/g,'')}'">` : (it.thumb?`<img src="${esc(it.thumb)}">`:'<span class=ph></span>');
function chip(label,ondel,num){ return `<span class="chip${num?' num':''}">${esc(label)} <span class="x" onclick="${ondel}"></span></span>`; }
// crate-type colour (material_color from virtual_crate_type): blue crate #0000FF, wooden #BA906A, white tub #FFFFFF…
function hexRgb(h){ h=String(h||'').replace('#',''); if(h.length===3)h=h.split('').map(c=>c+c).join(''); const n=parseInt(h,16); return isNaN(n)?[30,144,255]:[(n>>16)&255,(n>>8)&255,n&255]; }
function rgba(h,a){ const [r,g,b]=hexRgb(h); return `rgba(${r},${g},${b},${a})`; }
function darken(h,f){ const [r,g,b]=hexRgb(h); return `rgb(${Math.round(r*f)},${Math.round(g*f)},${Math.round(b*f)})`; }
// facing convention (RECORDGOD_NAVIGATOR_PLAN §3)
const CELL=0.34;
@ -262,7 +266,9 @@ function drawRack(){
const {lv,label}=levelMeta();
$('#lvlCtl').style.display = lv>1?'inline-flex':'none';
$('#lvlLabel').textContent=label; $('#lvlPrev').disabled=ST.level<=1; $('#lvlNext').disabled=ST.level>=lv;
$('#legend').innerHTML='<span><i class="sw" style="background:#9ecbff"></i>crate</span><span>↑↓←→ = facing · green=items'+(ST.tab==='reorganize'?' · <b class="pink">click in order to pick</b>':ST.tab==='collections'&&ST.edit?' · <b class="pink">click to add to collection</b>':'')+'</span>';
const types=[...new Map((ST.crates||[]).map(c=>[c.crate_type||'crate', c.color||'#1e90ff'])).entries()];
$('#legend').innerHTML=types.map(([n,c])=>`<span><i class="sw" style="background:${esc(c)};border:1px solid #aaa"></i>${esc(n)}</span>`).join('')
+'<span>↑↓←→ = facing</span>'+(ST.tab==='reorganize'?'<span><b class="pink">click in order to pick</b></span>':ST.tab==='collections'&&ST.edit?'<span><b class="pink">click to add to collection</b></span>':'');
const cv=$('#cv'), g=cv.getContext('2d'), W=cv.width, H=cv.height;
g.clearRect(0,0,W,H); g.fillStyle='#fbfbfd'; g.fillRect(0,0,W,H);
ST.hit=[];
@ -286,16 +292,16 @@ function drawRack(){
if(row>=rows) return;
const x=oX+col*cellW+3, y=oY+row*cellH+3, w=cellW-6, h=cellH-6;
const sel=ST.selCrate===c.id;
const picked=ST.tab==='reorganize' && ST.pick.indexOf(c.id);
const inColl=ST.tab==='collections' && ST.edit && ST.edit.crate_ids.includes(c.id);
g.fillStyle = inColl?'rgba(70,180,90,.28)' : sel?'rgba(255,46,147,.30)' : 'rgba(30,144,255,.16)';
g.strokeStyle = inColl?'#2e8b57' : sel?'#d10f7a' : '#1e90ff'; g.lineWidth=sel?3:1.5;
const tcol=c.color||'#1e90ff'; // crate-type material colour
g.fillStyle = inColl?'rgba(70,180,90,.28)' : sel?'rgba(255,46,147,.30)' : rgba(tcol,0.34);
g.strokeStyle = inColl?'#2e8b57' : sel?'#d10f7a' : darken(tcol,0.55); g.lineWidth=sel?3:1.5;
g.beginPath(); g.rect(x,y,w,h); g.fill(); g.stroke();
g.fillStyle='#1b1b22'; g.font='bold 10px system-ui'; g.textAlign='center'; g.textBaseline='top';
g.fillText('#'+c.id, x+w/2, y+4);
const gl=(c.label_text||c.crate_purpose||'').toString().split(',')[0].slice(0,12);
g.fillStyle='#555'; g.font='9px system-ui'; g.fillText(gl||'—', x+w/2, y+16);
g.fillStyle=sel?'#d10f7a':'#1e90ff'; g.font='bold 20px system-ui'; g.textBaseline='middle';
g.fillStyle=sel?'#d10f7a':darken(tcol,0.55); g.font='bold 20px system-ui'; g.textBaseline='middle';
g.fillText(ARROW[(c.direction||'').toLowerCase()]||'•', x+w/2, y+h-13);
g.fillStyle='#1a8f54'; g.font='9px system-ui'; g.textAlign='right'; g.textBaseline='top';
g.fillText(slot, x+w-3, y+3);