BREP_mcp_core 0.3.0

The BREP MCP server core: the Model Context Protocol surface generated from the CAD app's own registries, its sessions, script runner and transports (stdio and streamable HTTP). Embedded in the app behind `brep-app --mcp`; hosted headlessly by BREP_mcp.
Documentation
//! Screenshot post-processing: PNG encode/decode, crop, downscale, diff, and a
//! drawn cursor. Pure CPU, `image` crate, no fonts (the set-of-marks
//! annotation is Phase 5).
use image::{imageops, ImageBuffer, Rgba, RgbaImage};

/// A rectangle in pixels.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PxRect {
    pub x: u32,
    pub y: u32,
    pub w: u32,
    pub h: u32,
}

pub fn decode_png(bytes: &[u8]) -> Result<RgbaImage, String> {
    image::load_from_memory_with_format(bytes, image::ImageFormat::Png)
        .map(|i| i.to_rgba8())
        .map_err(|e| format!("png decode: {e}"))
}

pub fn encode_png(img: &RgbaImage) -> Result<Vec<u8>, String> {
    let mut out = std::io::Cursor::new(Vec::new());
    img.write_to(&mut out, image::ImageFormat::Png)
        .map_err(|e| format!("png encode: {e}"))?;
    Ok(out.into_inner())
}

/// Crop to `rect`, clamped to the image.
pub fn crop(img: &RgbaImage, rect: PxRect) -> RgbaImage {
    let x = rect.x.min(img.width().saturating_sub(1));
    let y = rect.y.min(img.height().saturating_sub(1));
    let w = rect.w.min(img.width() - x).max(1);
    let h = rect.h.min(img.height() - y).max(1);
    imageops::crop_imm(img, x, y, w, h).to_image()
}

/// Downscale so the width fits `max_width` (never upscales).
pub fn scale_to_width(img: &RgbaImage, max_width: u32) -> RgbaImage {
    if img.width() <= max_width || max_width == 0 {
        return img.clone();
    }
    let h = (img.height() as f64 * max_width as f64 / img.width() as f64).round().max(1.0) as u32;
    imageops::resize(img, max_width, h, imageops::FilterType::Triangle)
}

/// Fraction of differing pixels (any channel differs by more than `threshold`)
/// and the bounding box of the difference, after scaling `b` to `a`'s size.
pub struct Diff {
    pub ratio: f64,
    pub bbox: Option<PxRect>,
    pub mask: RgbaImage,
}

pub fn diff(a: &RgbaImage, b: &RgbaImage, threshold: u8) -> Diff {
    let b = if b.dimensions() != a.dimensions() {
        imageops::resize(b, a.width(), a.height(), imageops::FilterType::Triangle)
    } else {
        b.clone()
    };
    let (w, h) = a.dimensions();
    let mut mask: RgbaImage = ImageBuffer::from_pixel(w, h, Rgba([0, 0, 0, 255]));
    let mut count = 0u64;
    let (mut x0, mut y0, mut x1, mut y1) = (u32::MAX, u32::MAX, 0u32, 0u32);
    for y in 0..h {
        for x in 0..w {
            let pa = a.get_pixel(x, y).0;
            let pb = b.get_pixel(x, y).0;
            let differs = (0..3).any(|c| pa[c].abs_diff(pb[c]) > threshold);
            if differs {
                count += 1;
                mask.put_pixel(x, y, Rgba([255, 0, 255, 255]));
                x0 = x0.min(x);
                y0 = y0.min(y);
                x1 = x1.max(x);
                y1 = y1.max(y);
            }
        }
    }
    let bbox = (count > 0).then(|| PxRect { x: x0, y: y0, w: x1 - x0 + 1, h: y1 - y0 + 1 });
    Diff { ratio: count as f64 / (w as f64 * h as f64), bbox, mask }
}

/// Draw a simple arrow cursor with its tip at (x, y): white fill, black
/// outline, so it reads on any background.
pub fn draw_cursor(img: &mut RgbaImage, x: i64, y: i64, pressed: bool) {
    const SHAPE: [&str; 12] = [
        "X...........",
        "XX..........",
        "XOX.........",
        "XOOX........",
        "XOOOX.......",
        "XOOOOX......",
        "XOOOOOX.....",
        "XOOOOOOX....",
        "XOOOOOOOX...",
        "XOOOOXXXXX..",
        "XOOXOX......",
        "XX..XOX.....",
    ];
    let fill = if pressed { Rgba([255, 220, 0, 255]) } else { Rgba([255, 255, 255, 255]) };
    for (dy, row) in SHAPE.iter().enumerate() {
        for (dx, ch) in row.chars().enumerate() {
            let px = x + dx as i64;
            let py = y + dy as i64;
            if px < 0 || py < 0 || px >= img.width() as i64 || py >= img.height() as i64 {
                continue;
            }
            match ch {
                'X' => img.put_pixel(px as u32, py as u32, Rgba([0, 0, 0, 255])),
                'O' => img.put_pixel(px as u32, py as u32, fill),
                _ => {}
            }
        }
    }
}

/// Is the image (nearly) a single flat colour? A black frame from a failed
/// render trips this; a rendered app never does.
pub fn is_flat(img: &RgbaImage) -> bool {
    let Some(first) = img.pixels().next() else { return true };
    img.pixels().all(|p| p.0[..3] == first.0[..3])
}

// BREP private tests: ea12e628666aae53