BREP_app 0.4.0

The BREP CAD application: an eframe (egui + wgpu) host that draws the brep-render 3D engine into an egui frame — native + wasm from one codebase.
Documentation
//! Scene queries and selection: what exists, what is under a point, what is
//! selected, mass properties. Points are egui surface points; the viewport
//! offset is applied here.
use crate::automation::command::{parse_args, schema_of, Annotations, CommandSpec, Ctx, Empty, Handler, NoArgs, Outcome, Phase};
use crate::automation::cmd_document::parse;
use serde::Deserialize;
use serde_json::{json, Value};

#[derive(Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct PointArgs {
    pub x: f32,
    pub y: f32,
}

#[derive(Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct SelectArgs {
    /// `solid` | `face` | `edge` | `datum`
    pub kind: String,
    pub name: String,
}

/// The whole selection at once — the shape `selection` returns, minus the
/// position-keyed vertices.
#[derive(Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct SelectionSetArgs {
    #[serde(default)]
    pub solids: Vec<String>,
    #[serde(default)]
    pub faces: Vec<String>,
    #[serde(default)]
    pub edges: Vec<String>,
    #[serde(default)]
    pub datums: Vec<String>,
}

#[derive(Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct VisibleArgs {
    pub name: String,
    pub visible: bool,
}

#[derive(Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct MassArgs {
    /// A solid name; omitted = every resident solid.
    #[serde(default)]
    pub name: Option<String>,
    #[serde(default = "one")]
    pub density: f64,
}
fn one() -> f64 {
    1.0
}

/// Surface point → viewport-local point (what the engine's pickers take).
fn local(ctx: &Ctx<'_>, x: f32, y: f32) -> Result<(f64, f64), String> {
    let r = ctx.app.viewport.last_rect().ok_or("the 3D viewport was not drawn last frame")?;
    Ok(((x - r.min.x) as f64, (y - r.min.y) as f64))
}

fn scene_entities(ctx: &mut Ctx<'_>, args: Value) -> Result<Outcome, String> {
    let _: NoArgs = parse_args(args)?;
    let engine = ctx.app.docs.engine();
    Ok(Outcome::Done(json!({
        "solids": parse(&engine.scene_entities_json()),
        "listing": parse(&engine.scene_listing_json()),
    })))
}

fn pick(ctx: &mut Ctx<'_>, args: Value) -> Result<Outcome, String> {
    let a: PointArgs = parse_args(args)?;
    let (lx, ly) = local(ctx, a.x, a.y)?;
    Ok(Outcome::Done(json!({ "candidates": parse(&ctx.app.docs.engine().pick_json(lx, ly)) })))
}

fn hover(ctx: &mut Ctx<'_>, args: Value) -> Result<Outcome, String> {
    let a: PointArgs = parse_args(args)?;
    let (lx, ly) = local(ctx, a.x, a.y)?;
    let changed = ctx.app.docs.engine_mut().hover_at(lx, ly);
    Ok(Outcome::Done(json!({ "changed": changed, "candidate": parse(&ctx.app.docs.engine().hover_json(lx, ly)) })))
}

fn select(ctx: &mut Ctx<'_>, args: Value) -> Result<Outcome, String> {
    let a: SelectArgs = parse_args(args)?;
    let ok = ctx.app.docs.engine_mut().select_by_name(&a.kind, &a.name);
    if !ok {
        return Err(format!("nothing selected: unknown kind `{}` or name `{}`", a.kind, a.name));
    }
    Ok(Outcome::Done(json!({ "selection": parse(&ctx.app.docs.engine().selection_json()) })))
}

fn selection_set(ctx: &mut Ctx<'_>, args: Value) -> Result<Outcome, String> {
    let a: SelectionSetArgs = parse_args(args)?;
    ctx.app
        .docs
        .engine_mut()
        .set_selection(&a.solids, &a.faces, &a.edges, &a.datums);
    Ok(Outcome::Done(json!({ "selection": parse(&ctx.app.docs.engine().selection_json()) })))
}

fn select_clear(ctx: &mut Ctx<'_>, args: Value) -> Result<Outcome, String> {
    let _: NoArgs = parse_args(args)?;
    let changed = ctx.app.docs.engine_mut().clear_selection();
    Ok(Outcome::Done(json!({ "changed": changed })))
}

fn selection(ctx: &mut Ctx<'_>, args: Value) -> Result<Outcome, String> {
    let _: NoArgs = parse_args(args)?;
    Ok(Outcome::Done(json!({ "selection": parse(&ctx.app.docs.engine().selection_json()) })))
}

fn set_visible(ctx: &mut Ctx<'_>, args: Value) -> Result<Outcome, String> {
    let a: VisibleArgs = parse_args(args)?;
    let ok = ctx.app.docs.engine_mut().set_visible(&a.name, a.visible);
    if !ok {
        return Err(format!("no scene entity `{}`", a.name));
    }
    Ok(Outcome::Done(json!({}))) 
}

#[derive(Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct LocateArgs {
    /// `solid` | `face` | `edge` | `vertex`
    pub kind: String,
    /// The entity name (a vertex takes its topoId as a number string).
    pub name: String,
}

/// The world point that stands for an entity: a face's display-triangle
/// centroid, an edge's polyline midpoint, a vertex's position, a solid's
/// bounding-box centre.
fn anchor_point(scene: &brep_render::scene::RenderScene, kind: &str, name: &str) -> Result<[f64; 3], String> {
    match kind {
        "solid" => {
            let solid = scene.solid(name).ok_or_else(|| format!("no solid `{name}`"))?;
            let pos = &solid.mesh.positions;
            if pos.is_empty() {
                return Err(format!("solid `{name}` has no display mesh"));
            }
            let mut lo = [f64::MAX; 3];
            let mut hi = [f64::MIN; 3];
            for p in pos {
                for i in 0..3 {
                    lo[i] = lo[i].min(p[i] as f64);
                    hi[i] = hi[i].max(p[i] as f64);
                }
            }
            Ok([(lo[0] + hi[0]) / 2.0, (lo[1] + hi[1]) / 2.0, (lo[2] + hi[2]) / 2.0])
        }
        "face" => {
            for solid in scene.solids() {
                if let Some(face) = solid.faces.iter().find(|f| f.name == name) {
                    let idx = &solid.mesh.indices;
                    let pos = &solid.mesh.positions;
                    let start = (face.tri_start as usize) * 3;
                    let end = ((face.tri_start + face.tri_count) as usize * 3).min(idx.len());
                    if end <= start {
                        return Err(format!("face `{name}` has no triangles"));
                    }
                    let mut acc = [0.0f64; 3];
                    let mut n = 0.0;
                    for &i in &idx[start..end] {
                        if let Some(p) = pos.get(i as usize) {
                            for k in 0..3 {
                                acc[k] += p[k] as f64;
                            }
                            n += 1.0;
                        }
                    }
                    return Ok([acc[0] / n, acc[1] / n, acc[2] / n]);
                }
            }
            Err(format!("no face `{name}`"))
        }
        "edge" => {
            for solid in scene.solids() {
                if let Some(edge) = solid.edges.iter().find(|e| e.name == name) {
                    let pl = &edge.polyline;
                    if pl.is_empty() {
                        return Err(format!("edge `{name}` has no polyline"));
                    }
                    let p = pl[pl.len() / 2];
                    return Ok([p[0] as f64, p[1] as f64, p[2] as f64]);
                }
            }
            Err(format!("no edge `{name}`"))
        }
        "vertex" => {
            let id: u64 = name.parse().map_err(|_| format!("vertex name must be a topoId number, got `{name}`"))?;
            for solid in scene.solids() {
                if let Some(v) = solid.vertices.iter().find(|v| v.topo_id == id) {
                    return Ok(v.position);
                }
            }
            Err(format!("no vertex with topoId {id}"))
        }
        other => Err(format!("kind `{other}` must be solid | face | edge | vertex")),
    }
}

fn locate(ctx: &mut Ctx<'_>, args: Value) -> Result<Outcome, String> {
    let a: LocateArgs = parse_args(args)?;
    let view = ctx.app.viewport.last_rect().ok_or("the 3D viewport was not drawn last frame")?;
    let engine = ctx.app.docs.engine();
    let world = anchor_point(&engine.scene, &a.kind, &a.name)?;
    let projected = engine.world_to_screen_json(&json!([world]).to_string())?;
    let pts: Vec<[f64; 4]> = serde_json::from_str(&projected).map_err(|e| format!("projection parse: {e}"))?;
    let p = pts.first().ok_or("projection returned nothing")?;
    let (x, y) = (view.min.x as f64 + p[0], view.min.y as f64 + p[1]);
    let visible = p[3] > 0.5;
    // What a click there would actually hit — the caller learns when the
    // anchor is occluded by something nearer.
    let under: Value = parse(&engine.pick_json(p[0], p[1]));
    let top = under.as_array().and_then(|c| c.first()).cloned().unwrap_or(Value::Null);
    let occluded = visible && top.get("name").and_then(Value::as_str) != Some(a.name.as_str()) && !top.is_null();
    Ok(Outcome::Done(json!({
        "kind": a.kind,
        "name": a.name,
        "x": x,
        "y": y,
        "depth": p[2],
        "visible": visible,
        "world": world,
        "occluded_by": if occluded { top } else { Value::Null },
    })))
}

fn mass_properties(ctx: &mut Ctx<'_>, args: Value) -> Result<Outcome, String> {
    let a: MassArgs = parse_args(args)?;
    let v = parse(&ctx.app.docs.engine().mass_properties_json(a.name.as_deref(), a.density));
    if v.get("ok") == Some(&Value::Bool(false)) {
        return Err(v.get("message").and_then(Value::as_str).unwrap_or("mass properties unavailable").to_string());
    }
    Ok(Outcome::Done(v))
}

pub static COMMANDS: &[CommandSpec] = &[
    CommandSpec { name: "scene_entities", group: "scene", doc: "Every resident solid with its face and edge names and vertex positions (the reference names feature parameters take), plus the scene listing.", phase: Phase::Read, annotations: Annotations::READ, args_schema: schema_of::<NoArgs>, result_schema: schema_of::<Empty>, handler: Handler::App(scene_entities) },
    CommandSpec { name: "pick", group: "scene", doc: "Ranked pick candidates under a surface point (VERTEX > EDGE > FACE > PLANE > SOLID > COMPONENT). Does not change the selection.", phase: Phase::Read, annotations: Annotations::READ, args_schema: schema_of::<PointArgs>, result_schema: schema_of::<Empty>, handler: Handler::App(pick) },
    CommandSpec { name: "hover", group: "scene", doc: "Set the hover highlight to whatever is under a surface point and return it.", phase: Phase::Mutate, annotations: Annotations::MUTATE_NOWAIT, args_schema: schema_of::<PointArgs>, result_schema: schema_of::<Empty>, handler: Handler::App(hover) },
    CommandSpec { name: "select", group: "scene", doc: "Replace the selection with one named entity: kind solid | face | edge | datum.", phase: Phase::Mutate, annotations: Annotations::MUTATE_NOWAIT, args_schema: schema_of::<SelectArgs>, result_schema: schema_of::<Empty>, handler: Handler::App(select) },
    CommandSpec { name: "selection_set", group: "scene", doc: "Replace the WHOLE selection with these named entities — the write twin of `selection`, and the way to put a multi-entity selection back (`select` takes one name). Vertices have no names and are not restored.", phase: Phase::Mutate, annotations: Annotations::MUTATE_NOWAIT, args_schema: schema_of::<SelectionSetArgs>, result_schema: schema_of::<Empty>, handler: Handler::App(selection_set) },
    CommandSpec { name: "select_clear", group: "scene", doc: "Clear the selection.", phase: Phase::Mutate, annotations: Annotations::MUTATE_NOWAIT, args_schema: schema_of::<NoArgs>, result_schema: schema_of::<Empty>, handler: Handler::App(select_clear) },
    CommandSpec { name: "selection", group: "scene", doc: "The current selection `{solids, faces, edges, datums, vertices}`.", phase: Phase::Read, annotations: Annotations::READ, args_schema: schema_of::<NoArgs>, result_schema: schema_of::<Empty>, handler: Handler::App(selection) },
    CommandSpec { name: "set_visible", group: "scene", doc: "Show or hide a scene entity by name.", phase: Phase::Mutate, annotations: Annotations::MUTATE_NOWAIT, args_schema: schema_of::<VisibleArgs>, result_schema: schema_of::<Empty>, handler: Handler::App(set_visible) },
    CommandSpec { name: "locate", group: "scene", doc: "Where an entity is on the surface, in egui points: a face's triangle centroid, an edge's midpoint, a vertex (by topoId), a solid's bbox centre — projected through the camera, with what a click there would hit if something nearer occludes it. The bridge from a reference name to a pointer click.", phase: Phase::Read, annotations: Annotations::READ, args_schema: schema_of::<LocateArgs>, result_schema: schema_of::<Empty>, handler: Handler::App(locate) },
    CommandSpec { name: "mass_properties", group: "scene", doc: "Volume, surface area, mass, centroid, inertia and principal moments of one solid or of all — the closed-form check for an example model.", phase: Phase::Read, annotations: Annotations::READ, args_schema: schema_of::<MassArgs>, result_schema: schema_of::<Empty>, handler: Handler::App(mass_properties) },
];