clamp dropped images to grid bounds instead of wrapping to 0,0

Hunter
Wednesday, July 29th, 2026 at 3:17 PM

resources/images.js+20-13
resources/input.js+5-7

resources/images.js
 79  		imageDataArray.push({ idx, ...data });
 80  	}
 81  
 82 +	// Shift the whole fan back onto the grid before placing anything, so a drop near an edge keeps
 83 +	// its diagonal spacing instead of collapsing image by image
 84 +	const fanMaxX = Math.max(...imageDataArray.map(({ idx, widthCells }) => baseXCell + idx + widthCells));
 85 +	const fanMaxY = Math.max(...imageDataArray.map(({ idx, heightCells }) => baseYCell + idx + heightCells));
 86 +	const fanShiftX = Math.max(Math.min(0, GRID_COLS - fanMaxX), -baseXCell);
 87 +	const fanShiftY = Math.max(Math.min(0, GRID_ROWS - fanMaxY), -baseYCell);
 88 +
 89  	let wrappedOffset = 0;
 90 +	const placed = [];
 91  
 92  	for (const { idx, dataUrl, widthCells, heightCells, naturalWidth, naturalHeight } of imageDataArray) {
    -		// Calculate position with diagonal offset
    -		let xCell = baseXCell + idx;
    -		let yCell = baseYCell + idx;
    -
    -		// If out of bounds or would overlap with wrapped images, wrap to next diagonal position
    -		const outOfBounds = xCell < 0 || yCell < 0 ||
    -		                    xCell + widthCells > GRID_COLS ||
    -		                    yCell + heightCells > GRID_ROWS;
    -		const overlapsWrapped = xCell < wrappedOffset || yCell < wrappedOffset;
    -
    -		if (outOfBounds || overlapsWrapped) {
    -			xCell = wrappedOffset;
    -			yCell = wrappedOffset;
 93 +		// Calculate position with diagonal offset, kept as close to the drop point as the grid allows
 94 +		const maxXCell = GRID_COLS - widthCells;
 95 +		const maxYCell = GRID_ROWS - heightCells;
 96 +		let xCell = Math.max(0, Math.min(baseXCell + idx + fanShiftX, maxXCell));
 97 +		let yCell = Math.max(0, Math.min(baseYCell + idx + fanShiftY, maxYCell));
 98 +
 99 +		// Clamping can pile a multi-image drop onto one cell; cascade those from the top-left instead
100 +		const stacked = placed.some(p => p.xCell === xCell && p.yCell === yCell);
101 +		if (stacked) {
102 +			xCell = Math.min(wrappedOffset, maxXCell);
103 +			yCell = Math.min(wrappedOffset, maxYCell);
104  			wrappedOffset++;
105  		}
106 +		placed.push({ xCell, yCell });
107  
108  		const imageData = addImage(dataUrl, xCell, yCell, widthCells, heightCells, { naturalWidth, naturalHeight });
109  		imageData.container.style.zIndex = baseZIndex + idx;
resources/input.js
26  		return;
27  	}
28  
   -	// Use last known mouse position relative to grid, fall back to (0, 0)
29 +	// Use last known mouse position relative to grid; off-grid positions are clamped downstream,
30 +	// so only a cursor we've never seen falls back to (0, 0)
31  	const gridRect = grid.getBoundingClientRect();
   -	const mouseXInGrid = lastMouseX - gridRect.left;
   -	const mouseYInGrid = lastMouseY - gridRect.top;
   -	const isOnGrid = mouseXInGrid >= 0 && mouseYInGrid >= 0 &&
   -		mouseXInGrid <= gridRect.width && mouseYInGrid <= gridRect.height;
32 +	const hasCursor = lastMouseX >= 0 && lastMouseY >= 0;
33  
   -	const dropX = isOnGrid ? mouseXInGrid : 0;
   -	const dropY = isOnGrid ? mouseYInGrid : 0;
34 +	const dropX = hasCursor ? lastMouseX - gridRect.left : 0;
35 +	const dropY = hasCursor ? lastMouseY - gridRect.top : 0;
36  
37  	await processAndAddImages(imageFiles, dropX, dropY);
38  });