BREP_kernel 0.4.0

A boundary representation (BREP) geometry kernel for building CAD applications.
Documentation
use super::*;

/// S5.0: the `eval_expression` re-export evaluates a dimension `valueExpr`
/// against the history's `expressions` variables (the live path the
/// engine-native sketcher uses). A bad expression surfaces an `Err`.
#[test]
fn eval_expression_evaluates_against_history_variables() {
    // A variable defined in the expressions source, referenced by the dim expr.
    assert_eq!(
        eval_expression("width = 10;", &serde_json::Value::Null, "width/2").unwrap(),
        5.0
    );
    // Configurator member access + a bare arithmetic expression.
    let cfg = serde_json::json!({ "values": { "d": 8 } });
    assert_eq!(eval_expression("", &cfg, "configurator.d + 2").unwrap(), 10.0);
    assert_eq!(eval_expression("", &serde_json::Value::Null, "10 + 5").unwrap(), 15.0);
    // An unknown identifier fails rather than silently yielding a number.
    assert!(eval_expression("", &serde_json::Value::Null, "nope * 2").is_err());
}

/// Stage-1 persistent handle registry: register two solids, union them BY
/// HANDLE (the result never crosses the boundary as topology), tessellate +
/// measure the resident result, then free — the registry returns to baseline
/// with no leak. Pins the resident-handle lifecycle end-to-end.
#[test]
fn solid_handle_registry_lifecycle() {
    // Two unit boxes overlapping in a corner (general position — no coplanar
    // faces): overlap = 0.5^3 = 0.125, so union volume = 1 + 1 - 0.125 = 1.875.
    let a = make_box_brep(Vec3::new(0.0, 0.0, 0.0), 1.0, 1.0, 1.0).unwrap();
    let b = make_box_brep(Vec3::new(0.5, 0.5, 0.5), 1.0, 1.0, 1.0).unwrap();
    let start = registered_solid_count();
    let ha = register_solid_value(a);
    let hb = register_solid_value(b);
    assert_ne!(ha, hb);
    assert_eq!(registered_solid_count(), start + 2);

    // Union by handle -> a NEW resident handle.
    let hc = boolean_handle(ha, hb, "union", 1e-6, false).unwrap();
    assert_eq!(registered_solid_count(), start + 3);

    // Tessellation crosses the boundary; the solid stays resident.
    let mesh = tessellate_handle(hc, 0.01).unwrap();
    assert!(!mesh.positions.is_empty() && !mesh.indices.is_empty());

    // Analytic mass properties of the resident union.
    let props =
        with_registered_solid(hc, |s| solid_mass_properties(s).map_err(javascript_error))
            .unwrap();
    assert!(
        (props.volume - 1.875).abs() < 1e-6,
        "union volume {} != 1.875",
        props.volume
    );

    // Stage 1b: transform_handle -> a NEW resident handle; a rigid translation
    // preserves volume. face_names_handle returns a non-empty id->name map.
    let identity_translate = [
        1.0, 0.0, 0.0, 5.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
    ];
    let ht = transform_handle(hc, &identity_translate, false).unwrap();
    assert_ne!(ht, hc);
    assert_eq!(registered_solid_count(), start + 4);
    let tprops =
        with_registered_solid(ht, |s| solid_mass_properties(s).map_err(javascript_error))
            .unwrap();
    assert!(
        (tprops.volume - 1.875).abs() < 1e-6,
        "translated volume {} != 1.875",
        tprops.volume
    );
    let names_json = face_names_handle(hc).unwrap();
    let names: Vec<(u64, Option<String>)> = serde_json::from_str(&names_json).unwrap();
    assert!(!names.is_empty(), "face_names_handle returned empty map");
    free_solid(ht);

    // Freeing an unknown handle is a safe no-op (JsValue-free path; the wasm
    // error paths that build a JsValue cannot run on the host test target).
    free_solid(u32::MAX);
    assert_eq!(registered_solid_count(), start + 3);

    // Explicit free returns the registry to baseline (no leak); idempotent.
    free_solid(ha);
    free_solid(hb);
    free_solid(hc);
    assert_eq!(registered_solid_count(), start);
    free_solid(hc);
    assert_eq!(registered_solid_count(), start);
}

#[test]
fn box_is_valid_and_has_exact_volume() {
    let mesh = make_box(2.0, 3.0, 4.0);
    mesh.validate().unwrap();
    assert!((mesh.signed_volume() - 24.0).abs() < 1e-12);
}

/// Volume of a mesh-only solid (no normal buffer — the sheet-metal metrics
/// mesh case): validate_geometry accepts it and signed_volume is exact,
/// whereas the full validate() correctly rejects the missing normals.
/// Pins the mesh_volume_json fix — volume must not require normals.
#[test]
fn mesh_volume_does_not_require_normals() {
    let mut mesh = make_box(2.0, 3.0, 4.0);
    mesh.normals.clear();
    assert!(
        mesh.validate().is_err(),
        "full validate() must still require normals"
    );
    mesh.validate_geometry()
        .expect("geometry validation must accept an empty normal buffer");
    assert!(
        (mesh.signed_volume() - 24.0).abs() < 1e-12,
        "normal-less mesh volume must be exact"
    );
}

#[test]
fn cylinder_converges_to_analytic_volume() {
    let mesh = make_cylinder(2.0, 5.0, 256);
    let expected = std::f64::consts::PI * 4.0 * 5.0;
    assert!((mesh.signed_volume() - expected).abs() / expected < 2e-4);
}

/// Native `export_step_handles`: register a box brep as a resident handle,
/// export it to STEP text BY HANDLE (the topology never leaves the registry),
/// and confirm the result is a valid ISO-10303-21 document that `import_step`
/// round-trips back to a single solid of the same volume. Empty handle list
/// and a freed handle both error rather than emitting bad STEP.
#[test]
fn export_step_handles_round_trips_a_resident_box() {
    let solid = make_box_brep(Vec3::new(0.0, 0.0, 0.0), 4.0, 3.0, 2.0).unwrap();
    let handle = register_solid_value(solid);
    let step = export_step_handles(&[handle], "Part", "MM", "").unwrap();
    assert!(step.contains("ISO-10303-21"), "STEP header present");

    let solids = import_step(&step).unwrap();
    assert_eq!(solids.len(), 1, "round-trips to one solid");
    let volume = solid_mass_properties(&solids[0]).unwrap().volume;
    assert!((volume - 24.0).abs() < 1e-6, "imported volume {volume} != 24");

    // Empty list is a clear error; a freed handle no longer exports.
    assert!(export_step_handles(&[], "Part", "MM", "").is_err());
    free_solid(handle);
    assert!(
        export_step_handles(&[handle], "Part", "MM", "").is_err(),
        "a freed handle must error, not emit stale STEP"
    );
}

/// A straight-segment line curve A→B (clamped degree-1).
fn line_curve(a: Vec3, b: Vec3) -> NurbsCurve {
    NurbsCurve::new(
        1,
        vec![0.0, 0.0, 1.0, 1.0],
        vec![Vec4::from_point(a, 1.0), Vec4::from_point(b, 1.0)],
    )
    .unwrap()
}

/// A sketch holding ONLY points (a hole-placement sketch) has no profile and no
/// segment, so it used to synthesize an EMPTY payload — invisible in 3D. Its
/// points now draw as vertices: one per distinct position, with no face and no
/// edge. A point coinciding with a segment endpoint the payload already drew
/// is not doubled.
#[test]
fn sketch_display_payload_draws_standalone_points_as_vertices() {
    let points = [
        Vec3::new(2.0, 2.0, 10.0),
        Vec3::new(8.0, 2.0, 10.0),
        Vec3::new(8.0, 8.0, 10.0),
    ];
    let payload = sketch_display_payload(None, &[], &points);
    assert!(payload.faces.is_empty() && payload.mesh.indices.is_empty(), "no face");
    assert!(payload.edges.is_empty(), "no edge");
    assert_eq!(payload.vertices.len(), 3, "one vertex per point");
    for ((_, drawn), want) in payload.vertices.iter().zip(points.iter()) {
        assert!(drawn.sub(*want).length() < 1e-12, "drawn where published");
    }

    // A point that is ALSO a segment endpoint draws once: the segment's
    // endpoints come first, the coincident point is deduped against them.
    let a = Vec3::new(0.0, 0.0, 0.0);
    let b = Vec3::new(10.0, 0.0, 0.0);
    let segments = vec![("Sk:G0".to_string(), vec![line_curve(a, b)])];
    let payload = sketch_display_payload(None, &segments, &[b, Vec3::new(5.0, 5.0, 0.0)]);
    assert_eq!(payload.edges.len(), 1, "the segment draws");
    assert_eq!(payload.vertices.len(), 3, "a + b from the segment, plus the one free point");
}

/// A committed sketch's profile becomes a SHEET-SOLID display payload: a
/// 10x6 rectangle on the XY plane yields ONE face (named `{sketchId}:FACE`),
/// FOUR named boundary edges, FOUR corner vertices, and a non-empty mesh.
#[test]
fn sketch_profile_display_payload_rectangle() {
    let corners = [
        Vec3::new(0.0, 0.0, 0.0),
        Vec3::new(10.0, 0.0, 0.0),
        Vec3::new(10.0, 6.0, 0.0),
        Vec3::new(0.0, 6.0, 0.0),
    ];
    let mut curves = Vec::new();
    let mut edge_names = Vec::new();
    for i in 0..4 {
        curves.push(line_curve(corners[i], corners[(i + 1) % 4]));
        edge_names.push(Some(format!("Sk:G{}", 10 + i)));
    }
    let profile = SketchProfile {
        origin: Vec3::new(0.0, 0.0, 0.0),
        x_axis: Vec3::new(1.0, 0.0, 0.0),
        y_axis: Vec3::new(0.0, 1.0, 0.0),
        z_axis: Vec3::new(0.0, 0.0, 1.0),
        regions: vec![vec![ProfileLoop {
            curves,
            edge_names,
            loop_id: None,
        }]],
    };

    let payload = sketch_profile_display_payload(&profile);
    assert_eq!(payload.faces.len(), 1, "one sheet face");
    assert_eq!(payload.faces[0].1.as_deref(), Some("Sk:FACE"));
    assert_eq!(payload.edges.len(), 4, "four boundary edges");
    assert_eq!(
        payload.edges[0].1.as_deref(),
        Some("Sk:G10"),
        "edge carries its sketch geometry name"
    );
    assert_eq!(payload.vertices.len(), 4, "four corner vertices");
    assert!(!payload.mesh.positions.is_empty(), "face mesh has vertices");
    assert!(!payload.mesh.indices.is_empty(), "face mesh has triangles");
    // Every triangle rides the single sheet face (id 0).
    assert_eq!(payload.mesh.face_ids.len(), payload.mesh.indices.len() / 3);
    assert!(payload.mesh.face_ids.iter().all(|&f| f == 0));
    // The triangulated area equals the rectangle's 60 mm² (divergence of the
    // planar mesh, projected on +Z).
    let area: f64 = payload
        .mesh
        .indices
        .chunks_exact(3)
        .map(|t| {
            let p = |i: u32| {
                let b = i as usize * 3;
                Vec3::new(
                    payload.mesh.positions[b],
                    payload.mesh.positions[b + 1],
                    payload.mesh.positions[b + 2],
                )
            };
            let (a, b, c) = (p(t[0]), p(t[1]), p(t[2]));
            b.sub(a).cross(c.sub(a)).length() * 0.5
        })
        .sum();
    assert!((area - 60.0).abs() < 1e-6, "sheet area {area} != 60");
}