use crate::scene::{solid_display_from_payload, RenderScene, SolidDisplay};
use brep_kernel::{display_payload_handle_native, execute_history, HistoryRequest};
use std::collections::HashMap;
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
pub struct SceneBuildReport {
pub feature_errors: Vec<String>,
pub unresolved: Vec<String>,
pub display_errors: Vec<String>,
pub feature_timings: Vec<(String, f64)>,
pub feature_outputs: Vec<(String, Vec<String>)>,
pub frames: Vec<(String, brep_kernel::Frame)>,
pub profiles: Vec<(String, brep_kernel::SketchProfile)>,
#[serde(default)]
pub axes: Vec<(String, brep_kernel::Axis)>,
}
pub fn scene_from_history_json(
request_json: &str,
) -> Result<(RenderScene, SceneBuildReport), String> {
let request: HistoryRequest = serde_json::from_str(request_json)
.map_err(|error| format!("history request parse: {error}"))?;
scene_from_history(&request)
}
pub fn scene_from_history(
request: &HistoryRequest,
) -> Result<(RenderScene, SceneBuildReport), String> {
let mut scene = RenderScene::new();
let report = update_scene_from_history(&mut scene, request, None)?;
Ok((scene, report))
}
struct SceneLayout {
order: Vec<String>,
handles: std::collections::HashMap<String, u32>,
reused: std::collections::HashSet<String>,
creators: std::collections::HashMap<String, String>,
}
fn fold_history(result: &brep_kernel::HistoryResult, report: &mut SceneBuildReport) -> SceneLayout {
let mut order: Vec<String> = Vec::new();
let mut handles: std::collections::HashMap<String, u32> = std::collections::HashMap::new();
let mut reused: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut creators: std::collections::HashMap<String, String> = std::collections::HashMap::new();
for feature in &result.results {
for removed in &feature.removed {
if handles.remove(removed).is_some() {
order.retain(|name| name != removed);
}
reused.remove(removed);
creators.remove(removed);
}
for added in &feature.added {
if handles.insert(added.name.clone(), added.handle).is_none() {
order.push(added.name.clone());
}
creators.insert(added.name.clone(), feature.id.clone());
if feature.reused {
reused.insert(added.name.clone());
} else {
reused.remove(&added.name);
}
}
if let Some(error) = &feature.error {
report.feature_errors.push(format!("{}: {error}", feature.id));
}
for name in &feature.unresolved {
report.unresolved.push(format!("{}: {name}", feature.id));
}
report.feature_outputs.push((
feature.id.clone(),
feature.added.iter().map(|a| a.name.clone()).collect(),
));
for (name, frame) in &feature.frames {
report.frames.push((name.clone(), *frame));
}
for (name, profile) in &feature.profiles {
report.profiles.push((name.clone(), profile.clone()));
}
for (name, axis) in &feature.axes {
report.axes.push((name.clone(), *axis));
}
}
report.feature_timings = result.timings.clone();
SceneLayout { order, handles, reused, creators }
}
pub struct SceneRunner {
last_sent: HashMap<String, u32>,
last_lod: f64,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct RunOutput {
pub snapshot: Vec<(String /* name */, u32 /* handle */, Option<SolidDisplay>)>,
pub report: SceneBuildReport,
pub provenance: Vec<(String, String)>,
}
impl SceneRunner {
pub fn new() -> Self {
Self { last_sent: HashMap::new(), last_lod: 1.0 }
}
pub fn reset(&mut self) {
self.last_sent.clear();
}
pub fn run(
&mut self,
request: &HistoryRequest,
color_overrides: Option<&HashMap<String, [f32; 3]>>,
) -> RunOutput {
let result = execute_history(request);
let mut report = SceneBuildReport::default();
let layout = fold_history(&result, &mut report);
let lod = if request.display_lod.is_finite() && request.display_lod > 0.0 {
request.display_lod
} else {
1.0
};
if lod != self.last_lod {
self.last_sent.clear();
self.last_lod = lod;
}
let mut snapshot: Vec<(String, u32, Option<SolidDisplay>)> =
Vec::with_capacity(layout.order.len());
let mut next_sent: HashMap<String, u32> = HashMap::with_capacity(layout.order.len());
for name in &layout.order {
let handle = layout.handles[name];
if self.last_sent.get(name) == Some(&handle) {
debug_assert!(
layout.reused.contains(name),
"handle unchanged must imply reused (monotonic handles): {name}"
);
snapshot.push((name.clone(), handle, None));
next_sent.insert(name.clone(), handle);
continue;
}
match display_payload_handle_native(handle, lod) {
Ok(payload) => {
let mut solid = solid_display_from_payload(name, payload);
solid.source_handle = handle;
if let Some(overrides) = color_overrides {
solid.color_override = overrides.get(name).copied();
}
snapshot.push((name.clone(), handle, Some(solid)));
next_sent.insert(name.clone(), handle);
}
Err(error) => report.display_errors.push(format!("{name}: {error}")),
}
}
self.last_sent = next_sent;
let provenance: Vec<(String, String)> = layout
.creators
.iter()
.map(|(name, id)| (name.clone(), id.clone()))
.collect();
RunOutput { snapshot, report, provenance }
}
pub fn handle_of(&self, name: &str) -> Option<u32> {
self.last_sent.get(name).copied()
}
}
impl Default for SceneRunner {
fn default() -> Self {
Self::new()
}
}
pub fn update_scene_from_history(
scene: &mut RenderScene,
request: &HistoryRequest,
color_overrides: Option<&HashMap<String, [f32; 3]>>,
) -> Result<SceneBuildReport, String> {
let mut runner = SceneRunner::new();
runner.last_sent = scene
.solids()
.iter()
.map(|solid| (solid.name.clone(), solid.source_handle))
.collect();
let output = runner.run(request, color_overrides);
let mut kept: HashMap<String, SolidDisplay> = scene
.drain()
.into_iter()
.map(|solid| (solid.name.clone(), solid))
.collect();
for (name, _handle, maybe) in output.snapshot {
match maybe {
Some(display) => scene.insert_solid(display),
None => scene.insert_solid(
kept.remove(&name).expect("keep target present"),
),
}
}
Ok(output.report)
}
pub fn resident_solid_handles(request: &HistoryRequest) -> Vec<(String, u32)> {
let result = execute_history(request);
let mut report = SceneBuildReport::default();
let layout = fold_history(&result, &mut report);
layout
.order
.iter()
.map(|name| (name.clone(), layout.handles[name]))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn cube_request(name: &str, size: f64) -> String {
serde_json::json!({
"expressions": "",
"configurator": {},
"features": [{
"type": "P.CU",
"inputParams": {
"id": name,
"sizeX": size, "sizeY": size, "sizeZ": size,
"transform": {
"position": [0.0, 0.0, 0.0],
"rotationEuler": [0.0, 0.0, 0.0],
"scale": [1.0, 1.0, 1.0]
},
"boolean": { "targets": [], "operation": "NONE" }
},
"persistentData": {}
}]
})
.to_string()
}
#[test]
fn dotted_feature_id_resolves_as_a_boolean_reference() {
let request = serde_json::json!({
"expressions": "",
"configurator": {},
"features": [
{
"type": "P.CU",
"inputParams": {
"id": "P.CU1",
"sizeX": 10.0, "sizeY": 10.0, "sizeZ": 10.0,
"transform": { "position": [0.0, 0.0, 0.0], "rotationEuler": [0.0, 0.0, 0.0], "scale": [1.0, 1.0, 1.0] },
"boolean": { "targets": [], "operation": "NONE" }
},
"persistentData": {}
},
{
"type": "P.CU",
"inputParams": {
"id": "P.CU2",
"sizeX": 10.0, "sizeY": 10.0, "sizeZ": 10.0,
"transform": { "position": [5.0, 0.0, 0.0], "rotationEuler": [0.0, 0.0, 0.0], "scale": [1.0, 1.0, 1.0] },
"boolean": { "targets": ["P.CU1"], "operation": "SUBTRACT" }
},
"persistentData": {}
}
]
})
.to_string();
let (scene, report) = scene_from_history_json(&request).unwrap();
assert!(report.feature_errors.is_empty(), "{:?}", report.feature_errors);
assert!(report.unresolved.is_empty(), "dotted ref unresolved: {:?}", report.unresolved);
assert_eq!(scene.solids().len(), 1);
}
#[test]
fn cube_history_populates_scene() {
let (scene, report) = scene_from_history_json(&cube_request("P.CU1", 10.0)).unwrap();
assert!(report.feature_errors.is_empty(), "{:?}", report.feature_errors);
assert!(report.display_errors.is_empty(), "{:?}", report.display_errors);
assert_eq!(scene.solids().len(), 1);
let solid = scene.solid("P.CU1").expect("solid keyed by kernel name");
assert_eq!(solid.faces.len(), 6);
assert_eq!(solid.mesh.indices.len(), 6 * 2 * 3);
for face in &solid.faces {
assert_eq!(face.tri_count, 2, "face {} range", face.name);
assert!(!face.name.is_empty());
}
assert_eq!(solid.edges.len(), 12);
for edge in &solid.edges {
assert!(edge.polyline.len() >= 2);
assert!(!edge.name.is_empty());
}
assert_eq!(solid.vertices.len(), 8);
let bbox = scene.bbox();
assert!((bbox.size()[0] - 10.0).abs() < 1e-6);
}
fn sphere_request(name: &str, radius: f64, lod: f64) -> brep_kernel::HistoryRequest {
let mut request: brep_kernel::HistoryRequest = serde_json::from_value(serde_json::json!({
"expressions": "",
"configurator": {},
"features": [{
"type": "P.S",
"inputParams": {
"id": name,
"radius": radius,
"transform": {
"position": [0.0, 0.0, 0.0],
"rotationEuler": [0.0, 0.0, 0.0],
"scale": [1.0, 1.0, 1.0]
},
"boolean": { "targets": [], "operation": "NONE" }
},
"persistentData": {}
}]
}))
.unwrap();
request.display_lod = lod;
request
}
#[test]
fn lod_change_retessellates_reused_solid_coarser() {
let mut runner = SceneRunner::new();
let fine = runner.run(&sphere_request("LodBall", 10.0, 0.5), None);
let (_, fine_handle, fine_display) = &fine.snapshot[0];
let fine_tris = fine_display.as_ref().expect("first run tessellates").mesh.indices.len();
let coarse = runner.run(&sphere_request("LodBall", 10.0, 4.0), None);
let (_, coarse_handle, coarse_display) = &coarse.snapshot[0];
assert_eq!(
coarse_handle, fine_handle,
"same sphere must replay to the same handle (reuse path) — else the test proves nothing"
);
let coarse_tris = coarse_display
.as_ref()
.expect("a lod change re-emits Some(display) despite the unchanged handle")
.mesh
.indices
.len();
assert!(
coarse_tris < fine_tris,
"coarser lod must shrink the mesh: fine(lod 0.5)={fine_tris} coarse(lod 4.0)={coarse_tris}"
);
let again = runner.run(&sphere_request("LodBall", 10.0, 4.0), None);
assert!(
again.snapshot[0].2.is_none(),
"an unchanged lod must reuse the display baseline (None), not re-tessellate"
);
}
#[test]
fn reused_history_keeps_stable_revision() {
let request: brep_kernel::HistoryRequest =
serde_json::from_str(&cube_request("Keep", 8.0)).unwrap();
let mut scene = RenderScene::new();
update_scene_from_history(&mut scene, &request, None).unwrap();
let first_rev = scene.solid("Keep").unwrap().revision;
update_scene_from_history(&mut scene, &request, None).unwrap();
assert_eq!(scene.solid("Keep").unwrap().revision, first_rev);
}
#[test]
fn color_override_applies_to_fresh_solids() {
let request: brep_kernel::HistoryRequest =
serde_json::from_str(&cube_request("Tinted", 5.0)).unwrap();
let mut overrides = std::collections::HashMap::new();
overrides.insert("Tinted".to_string(), [1.0, 0.0, 0.0]);
let mut scene = RenderScene::new();
update_scene_from_history(&mut scene, &request, Some(&overrides)).unwrap();
assert_eq!(scene.solid("Tinted").unwrap().color_override, Some([1.0, 0.0, 0.0]));
}
#[test]
fn scene_insert_replace_and_remove() {
let (mut scene, _) = scene_from_history_json(&cube_request("A", 4.0)).unwrap();
let (other, _) = scene_from_history_json(&cube_request("B", 2.0)).unwrap();
for solid in other.solids() {
scene.insert_solid(solid.clone());
}
assert_eq!(scene.solids().len(), 2);
assert!(scene.remove_solid("A"));
assert!(!scene.remove_solid("A"));
assert_eq!(scene.solids().len(), 1);
assert!(scene.solid("B").is_some());
}
#[test]
fn datum_history_surfaces_three_named_frames() {
let request = serde_json::json!({
"expressions": "",
"configurator": {},
"features": [{
"type": "D",
"inputParams": { "id": "Datum" },
"persistentData": {}
}]
})
.to_string();
let (_scene, report) = scene_from_history_json(&request).unwrap();
let names: Vec<&str> = report.frames.iter().map(|(n, _)| n.as_str()).collect();
assert!(names.contains(&"Datum:XY"), "{names:?}");
assert!(names.contains(&"Datum:XZ"), "{names:?}");
assert!(names.contains(&"Datum:YZ"), "{names:?}");
}
#[test]
fn run_output_round_trips_through_serde() {
let request: HistoryRequest = serde_json::from_str(
r#"{
"expressions": "", "configurator": {},
"features": [
{ "type": "P.CU", "inputParams": { "id": "Box", "sizeX": 10, "sizeY": 10, "sizeZ": 10,
"transform": { "position": [0,0,0], "rotationEuler": [0,0,0], "scale": [1,1,1] },
"boolean": { "targets": [], "operation": "NONE" } }, "persistentData": {} },
{ "type": "D", "inputParams": { "id": "Datum" }, "persistentData": {} },
{ "type": "S", "inputParams": { "id": "Sk" }, "persistentData": {
"basis": { "origin": [0,0,0], "x": [1,0,0], "y": [0,1,0], "z": [0,0,1] },
"sketch": { "points": [
{"id":0,"x":0,"y":0},{"id":1,"x":10,"y":0},{"id":2,"x":10,"y":6},{"id":3,"x":0,"y":6}],
"geometries": [
{"id":10,"type":"line","points":[0,1]},{"id":11,"type":"line","points":[1,2]},
{"id":12,"type":"line","points":[2,3]},{"id":13,"type":"line","points":[3,0]}],
"constraints": [] } } }
]
}"#,
)
.unwrap();
let output = SceneRunner::new().run(&request, None);
assert_eq!(output.snapshot.len(), 1, "one solid (the box)");
assert!(output.snapshot[0].2.as_ref().unwrap().mesh.positions.len() > 0);
assert!(output.report.frames.iter().any(|(n, _)| n == "Datum:XY"));
assert!(output.report.profiles.iter().any(|(n, _)| n == "Sk"));
assert!(output.provenance.iter().any(|(n, id)| n == "Box" && id == "Box"));
let json = serde_json::to_string(&output).expect("RunOutput serializes");
let back: RunOutput = serde_json::from_str(&json).expect("RunOutput deserializes");
assert_eq!(back.snapshot.len(), output.snapshot.len());
assert_eq!(back.snapshot[0].0, "Box");
assert_eq!(
back.snapshot[0].2.as_ref().unwrap().mesh.positions.len(),
output.snapshot[0].2.as_ref().unwrap().mesh.positions.len()
);
assert!(back.report.frames.iter().any(|(n, _)| n == "Datum:XY"));
let (name, profile) = back.report.profiles.iter().find(|(n, _)| n == "Sk").unwrap();
assert_eq!(name, "Sk");
assert_eq!(profile.regions.len(), 1, "the rectangle profile survived");
assert_eq!(back.provenance, output.provenance);
}
}