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 @@
+
● Select Mode — click crates in order
@@ -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;