// Continuous circles and rotated fallen-log rectangles. // A uniform spatial index limits swept collision checks to nearby obstacles. import {W as WIDTH,H as HEIGHT} from './world-size.js?v=45'; const SIZE=120,EPS=.0001; const clamp=(v,a,b)=>Math.max(a,Math.min(b,v)); const local=(c,x,y)=>{const co=Math.cos(c.angle||0),si=Math.sin(c.angle||0),dx=x-c.x,dy=y-c.y;return {x:co*dx+si*dy,y:-si*dx+co*dy}}; const world=(c,x,y)=>{const co=Math.cos(c.angle||0),si=Math.sin(c.angle||0);return {x:c.x+co*x-si*y,y:c.y+si*x+co*y}}; const rectOverlap=(c,x,y,r)=>{const q=local(c,x,y),dx=Math.max(Math.abs(q.x)-c.w/2,0),dy=Math.max(Math.abs(q.y)-c.h/2,0);return dx*dx+dy*dy<(r-EPS)**2||(dx===0&&dy===0)}; function roundedRectHit(c,x,y,dx,dy,r){ const p=local(c,x,y),co=Math.cos(c.angle||0),si=Math.sin(c.angle||0),vx=co*dx+si*dy,vy=-si*dx+co*dy,hx=c.w/2,hy=c.h/2; let first=Infinity,nx=0,ny=0; const check=(t,a,b)=>{if(t>=0&&t=0||disc<0)continue;const t=(-b-Math.sqrt(disc))/len,xx=p.x+vx*t,yy=p.y+vy*t; if(sx*(xx-cx)<-EPS||sy*(yy-cy)<-EPS)continue; const n=Math.hypot(xx-cx,yy-cy)||1;check(t,(xx-cx)/n,(yy-cy)/n); } return first<=1?{t:first,x:co*nx-si*ny,y:si*nx+co*ny}:null; } const outputPosition=(out,x,y)=>{if(out){out.x=x;out.y=y;return out}return {x,y}}; export class RockField { constructor(circles=[],rects=[],preserveReferences=false){ this.cellSize=SIZE; this.nx=Math.ceil(WIDTH/SIZE);this.ny=Math.ceil(HEIGHT/SIZE); this.cells=Array.from({length:this.nx*this.ny},()=>[]); this.circles=preserveReferences?circles:circles.map(c=>({...c}));this.rects=rects.map(r=>({...r})); this.shapes=[...this.circles.map(c=>preserveReferences?c:{...c,kind:'circle'}),...this.rects.map(r=>({...r,kind:'rect'}))]; this.marks=new Uint32Array(this.shapes.length);this.stamp=0; for(let id=0;id>>0;if(!this.stamp){this.marks.fill(0);this.stamp=1} for(let x=Math.max(0,Math.floor((Math.min(x0,x1)-r)/SIZE));x<=Math.min(this.nx-1,Math.floor((Math.max(x0,x1)+r)/SIZE));x++)for(let y=Math.max(0,Math.floor((Math.min(y0,y1)-r)/SIZE));y<=Math.min(this.ny-1,Math.floor((Math.max(y0,y1)+r)/SIZE));y++)for(const id of this.cells[x*this.ny+y])if(this.marks[id]!==this.stamp){this.marks[id]=this.stamp;out.push(this.shapes[id])} return out; } free(x,y,r){ if(xWIDTH-r||yHEIGHT-r)return false; for(const c of this.candidates(x,y,x,y,r)){ if(c.kind==='rect'){if(rectOverlap(c,x,y,r))return false} else if((x-c.x)**2+(y-c.y)**2<(c.r+r-EPS)**2)return false; } return true; } freePoint(x,y){ if(x<0||x>WIDTH||y<0||y>HEIGHT)return false; const ix=Math.min(this.nx-1,Math.floor(x/SIZE)),iy=Math.min(this.ny-1,Math.floor(y/SIZE)); for(const id of this.cells[ix*this.ny+iy]){ const c=this.shapes[id]; if(c.kind==='rect'){if(rectOverlap(c,x,y,0))return false} else if((x-c.x)**2+(y-c.y)**2<(c.r-EPS)**2)return false; } return true; } findFree(x,y,r){ if(this.free(x,y,r))return {x,y};let px=clamp(x,r+EPS,WIDTH-r-EPS),py=clamp(y,r+EPS,HEIGHT-r-EPS); for(let pass=0;pass<16;pass++){ for(const c of this.candidates(px,py,px,py,r)){ if(c.kind==='rect'){ const q=local(c,px,py),left=-c.w/2-r-EPS,right=c.w/2+r+EPS,top=-c.h/2-r-EPS,bottom=c.h/2+r+EPS; if(q.x>left&&q.xtop&&q.ya.d-b.d),p=world(c,choices[0].x,choices[0].y);px=clamp(p.x,r+EPS,WIDTH-r-EPS);py=clamp(p.y,r+EPS,HEIGHT-r-EPS)} }else{const dx=px-c.x,dy=py-c.y,d=Math.hypot(dx,dy),limit=c.r+r+EPS;if(d{const d=(qx-x)**2+(qy-y)**2;if(d=0||disc<0)continue; const t=(-b-Math.sqrt(disc))/len;if(t>=-EPS&&t0&&distance