From d6c08f05ceb798363a74b287f103626fd347e7b2 Mon Sep 17 00:00:00 2001 From: type-two Date: Tue, 23 Jun 2026 21:55:51 +1000 Subject: [PATCH] =?UTF-8?q?feat(nav):=20rotate=20diagram=20button=20?= =?UTF-8?q?=E2=80=94=2090=C2=B0=20view=20rotation=20(rack=20+=20store),=20?= =?UTF-8?q?text=20stays=20upright?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ST.viewRot quarter-turns: drawRack remaps cells (rcell), edge labels (BACK/FRONT/L/R) and facing-arrow glyphs per rotation while keeping all text upright; drawStore rotates rack positions+yaw around centre with a matching unrotPt() so rack drag-to-move stays correct. 🔄 rotate button in the toolbar. Lets the diagram match where you're physically standing. Co-Authored-By: Claude Opus 4.8 --- site/search.html | 41 +++++++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/site/search.html b/site/search.html index 902655b..c83f2f8 100644 --- a/site/search.html +++ b/site/search.html @@ -94,6 +94,7 @@ + @@ -163,7 +164,7 @@ const ARROW={forward:'↓',front:'↓',north:'↓',back:'↑',south:'↑',left:' let ST={tab:'scanner', view:'store', space:null, racks:[], rack:null, level:1, crates:[], levels:[], selCrate:null, hit:[], storeTf:null, itemShown:false, - pick:[], pickNames:{}, edit:null, crateInfo:null, pickSlot:null, pickPos:'before'}; + pick:[], pickNames:{}, edit:null, crateInfo:null, pickSlot:null, pickPos:'before', viewRot:0}; async function signin(){ TOKEN=$('#tok').value.trim(); @@ -231,6 +232,7 @@ function setView(m){ if(m==='item'){ if(ST.itemShown){ ST.view='item'; setViewBtns(); $('#relBody').scrollIntoView({behavior:'smooth',block:'nearest'}); } return; } } function redraw(){ if(ST.view==='store') drawStore(); else if(ST.rack) drawRack(); } +function rotateView(){ ST.viewRot=((ST.viewRot||0)+90)%360; redraw(); } // ───────────── STORE VIEW ───────────── async function toStore(){ @@ -254,9 +256,12 @@ function drawStore(){ const pad=34, sc=Math.min((W-2*pad)/((maxX-minX)||1),(H-2*pad)/((maxZ-minZ)||1)); ST.storeTf={minX,minZ,sc,pad}; const X=x=>pad+(x-minX)*sc, Y=z=>pad+(z-minZ)*sc; + const q=(((ST.viewRot||0)/90)%4+4)%4; ST.racks.forEach(r=>{ - const yaw=directionYaw(r.direction)+(r.rot||0)*Math.PI/180; - const hw=(r.w||0.5)/2*sc, hd=(r.d||0.5)/2*sc, cx=X(r.x), cy=Y(r.z); + const yaw=directionYaw(r.direction)+(r.rot||0)*Math.PI/180 + q*Math.PI/2; + const hw=(r.w||0.5)/2*sc, hd=(r.d||0.5)/2*sc; + let cx=X(r.x), cy=Y(r.z); + if(q){ const a=q*Math.PI/2, co=Math.cos(a), si=Math.sin(a), dx=cx-W/2, dy=cy-H/2; cx=W/2+dx*co-dy*si; cy=H/2+dx*si+dy*co; } const cos=Math.cos(yaw), sin=Math.sin(yaw); const corners=[[-hw,-hd],[hw,-hd],[hw,hd],[-hw,hd]].map(([px,pz])=>[cx+px*cos-pz*sin, cy+px*sin+pz*cos]); g.beginPath(); g.moveTo(corners[0][0],corners[0][1]); corners.slice(1).forEach(c=>g.lineTo(c[0],c[1])); g.closePath(); @@ -298,23 +303,30 @@ function drawRack(){ ST.hit=[]; const rw=ST.rack.w||1.7, rd=ST.rack.d||0.7; const cols=Math.max(1,Math.floor(rw/CELL)), rows=Math.max(1,Math.round(rd/CELL)); - const pad=42, sc=0.92*Math.min((W-2*pad)/rw,(H-2*pad)/rd); - const cW=rw*sc, cH=rd*sc, oX=(W-cW)/2, oY=(H-cH)/2, cellW=cW/cols, cellH=cH/rows; + const q=(((ST.viewRot||0)/90)%4+4)%4; // view rotation (quarter turns CW; text stays upright) + const DC=q%2?rows:cols, DR=q%2?cols:rows; // displayed grid dims + const drw=q%2?rd:rw, drh=q%2?rw:rd; // displayed physical dims + const pad=42, sc=0.92*Math.min((W-2*pad)/drw,(H-2*pad)/drh); + const cW=drw*sc, cH=drh*sc, oX=(W-cW)/2, oY=(H-cH)/2, cellW=cW/DC, cellH=cH/DR; g.fillStyle='#fff'; g.strokeStyle='#222'; g.lineWidth=3; g.beginPath(); g.rect(oX,oY,cW,cH); g.fill(); g.stroke(); + const SIDES=[['BACK','R','FRONT','L'],['L','BACK','R','FRONT'],['FRONT','L','BACK','R'],['R','FRONT','L','BACK']][q]; // top,right,bottom,left g.fillStyle='#888'; g.font='11px system-ui'; g.textAlign='center'; - g.fillText('BACK',oX+cW/2,oY-8); g.fillText('FRONT',oX+cW/2,oY+cH+16); - g.save(); g.translate(oX-10,oY+cH/2); g.rotate(-Math.PI/2); g.fillText('L',0,0); g.restore(); - g.save(); g.translate(oX+cW+12,oY+cH/2); g.rotate(Math.PI/2); g.fillText('R',0,0); g.restore(); + g.fillText(SIDES[0],oX+cW/2,oY-8); g.fillText(SIDES[2],oX+cW/2,oY+cH+16); + g.save(); g.translate(oX-10,oY+cH/2); g.rotate(-Math.PI/2); g.fillText(SIDES[3],0,0); g.restore(); + g.save(); g.translate(oX+cW+12,oY+cH/2); g.rotate(Math.PI/2); g.fillText(SIDES[1],0,0); g.restore(); g.strokeStyle='#eee'; g.lineWidth=1; - for(let i=1;i q===0?[col,row] : q===1?[rows-1-row,col] : q===2?[cols-1-col,rows-1-row] : [row,cols-1-col]; + const rotGlyph=g0=>{ let gx=g0; for(let i=0;i(c.level==null?1:c.level)===ST.level); let autoSlot=0; here.forEach(c=>{ let slot=c.slot; if(!slot||slot<1){ autoSlot++; slot=autoSlot; } const idx=slot-1, col=idx%cols, row=Math.floor(idx/cols); if(row>=rows) return; - const x=oX+col*cellW+3, y=oY+row*cellH+3, w=cellW-6, h=cellH-6; + const [dx,dy]=rcell(col,row); + const x=oX+dx*cellW+3, y=oY+dy*cellH+3, w=cellW-6, h=cellH-6; const sel=ST.selCrate===c.id; const inColl=ST.tab==='collections' && ST.edit && ST.edit.crate_ids.includes(c.id); const tcol=c.color||'#1e90ff'; // crate-type material colour @@ -326,7 +338,7 @@ function drawRack(){ 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':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.fillText(rotGlyph(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); g.fillStyle='#888'; g.textAlign='left'; g.fillText((c.items||0), x+3, y+h-12); @@ -709,6 +721,7 @@ $('#omni').addEventListener('keydown',e=>{ if(e.key==='Enter') omni(); }); // ───────────── canvas: unified click + rack drag ───────────── function cvXY(e){ const cv=$('#cv'),r=cv.getBoundingClientRect(); return {mx:(e.clientX-r.left)*cv.width/r.width, my:(e.clientY-r.top)*cv.height/r.height}; } +function unrotPt(mx,my){ const q=(((ST.viewRot||0)/90)%4+4)%4; if(!q) return [mx,my]; const cv=$('#cv'),W=cv.width,H=cv.height,a=-q*Math.PI/2,c=Math.cos(a),s=Math.sin(a),dx=mx-W/2,dy=my-H/2; return [W/2+dx*c-dy*s, H/2+dx*s+dy*c]; } function hitRack(mx,my){ let best=null,bd=1e9; for(const h of ST.hit){ if(h.type!=='rack')continue; const dd=Math.hypot(mx-h.cx,my-h.cy); if(dd=h.x&&mx<=h.x+h.w&&my>=h.y&&my<=h.y+h.h) return h; } return null; } let down=null; @@ -720,8 +733,8 @@ $('#cv').addEventListener('mousemove',e=>{ if(!down||!down.dragRack) return; const {mx,my}=cvXY(e); if(Math.hypot(mx-down.mx,my-down.my)>5) down.moved=true; - if(down.moved){ const tf=ST.storeTf, r=ST.racks.find(x=>x.id===down.dragRack); - r.x=tf.minX+(mx-tf.pad)/tf.sc; r.z=tf.minZ+(my-tf.pad)/tf.sc; drawStore(); } + if(down.moved){ const tf=ST.storeTf, r=ST.racks.find(x=>x.id===down.dragRack), [wmx,wmy]=unrotPt(mx,my); + r.x=tf.minX+(wmx-tf.pad)/tf.sc; r.z=tf.minZ+(wmy-tf.pad)/tf.sc; drawStore(); } }); window.addEventListener('mouseup',async e=>{ if(!down) return; const d=down; down=null;