Files
bambuddy/frontend/src/utils/skipObjects.ts
T
maziggy 41ad1d65c7 feat(skip-objects): select items directly on the build plate
Pairs the top-down plate preview with the slicer's per-object pick mask
(Metadata/pick_N.png), whose pixel colours encode the same identify_id the
firmware's skip command takes, so a click resolves to a real object rather
than an inferred bounding box. Several objects can be selected before one
confirmation; selected and already-skipped items are highlighted on the
plate; the checklist stays available when no mask exists.

view=pick serves only the active plate's mask and 404s otherwise, unlike
every other view. A render returned in a mask's place would be decoded as
object IDs — dark pixels yield small integers that collide with real ones —
and a click would then skip an arbitrary object, mid-print, irreversibly.
The 404 is what tells the UI to fall back to the checklist.

Click mapping goes through the contained rect, since the canvas paints at
mask resolution under object-contain; clicks on a letterbox bar are rejected
rather than clamped onto whichever object touches the border. Confirming
names the object when one is selected and counts them when several are,
which is what plates of identically-named clones need.

No printer-control command path was added or changed; the layer, permission
and existing skip-command guards are untouched.
2026-07-22 12:32:14 +02:00

37 lines
1.6 KiB
TypeScript

export function pickObjectIdAt(imageData: ImageData, x: number, y: number): number | null {
const pixelX = Math.max(0, Math.min(imageData.width - 1, Math.floor(x)));
const pixelY = Math.max(0, Math.min(imageData.height - 1, Math.floor(y)));
const offset = (pixelY * imageData.width + pixelX) * 4;
const red = imageData.data[offset];
const green = imageData.data[offset + 1];
const blue = imageData.data[offset + 2];
const alpha = imageData.data[offset + 3];
if (!alpha || (red === 0 && green === 0 && blue === 0)) return null;
return red + (green << 8) + (blue << 16);
}
/**
* Map a click on the plate canvas to a pixel in the object-ID mask.
*
* The canvas paints at the mask's own resolution and is displayed with
* object-contain inside a square box, so a mask that isn't square is
* letterboxed. Measuring off the element rect would then offset every click by
* the size of the bars. Returns null for clicks on a bar: those are off the
* plate, and pickObjectIdAt would otherwise clamp them onto whichever object
* touches that border.
*/
export function plateClickToMaskPoint(
bounds: { left: number; top: number; width: number; height: number },
maskWidth: number,
maskHeight: number,
clientX: number,
clientY: number,
): { x: number; y: number } | null {
const scale = Math.min(bounds.width / maskWidth, bounds.height / maskHeight);
if (!(scale > 0)) return null;
const x = (clientX - bounds.left - (bounds.width - maskWidth * scale) / 2) / scale;
const y = (clientY - bounds.top - (bounds.height - maskHeight * scale) / 2) / scale;
if (x < 0 || y < 0 || x >= maskWidth || y >= maskHeight) return null;
return { x, y };
}