#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct RunReply {
pub generation: u64,
pub output: crate::pipeline::RunOutput,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum MeasureKind {
Solid,
Face,
Edge,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct MeasureQuery {
pub id: u64,
pub kind: MeasureKind,
pub owner: String,
pub entity: String,
pub density: f64,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct MeasureReply {
pub id: u64,
pub result: String,
}
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
pub enum MeshImportFormat {
Stl,
Obj,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct MeshImportRequest {
pub id: u64,
pub format: MeshImportFormat,
pub bytes: Vec<u8>,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct MeshImportReply {
pub id: u64,
pub result: Result<String, String>,
}
pub trait HistoryRunner {
fn submit_run(&mut self, request: brep_kernel::HistoryRequest, generation: u64);
fn poll_run(&mut self) -> Option<RunReply>;
fn submit_query(&mut self, query: MeasureQuery);
fn poll_query(&mut self) -> Option<MeasureReply>;
fn submit_mesh_import(&mut self, request: MeshImportRequest);
fn poll_mesh_import(&mut self) -> Option<MeshImportReply>;
fn reset(&mut self);
fn sync_parts_library(
&mut self,
_revision: u64,
_fetch: &mut dyn FnMut() -> brep_kernel::PartsLibraryMap,
) {
}
fn poll_library_request(&mut self) -> bool {
false
}
}
fn measure_json(runner: &crate::pipeline::SceneRunner, query: &MeasureQuery) -> String {
let Some(handle) = runner.handle_of(&query.owner) else {
return serde_json::json!({
"ok": false,
"message": format!("solid '{}' has no resident geometry", query.owner),
})
.to_string();
};
match query.kind {
MeasureKind::Solid => {
match brep_kernel::mass_properties_handle_native(handle, query.density) {
Ok(properties) => {
let edge_total =
brep_kernel::solid_edge_length_total_native(handle).unwrap_or(0.0);
serde_json::json!({
"ok": true,
"kind": "solid",
"volume": properties.volume,
"surfaceArea": properties.surface_area,
"edgeLengthTotal": edge_total,
"density": properties.density,
"weight": properties.mass,
})
.to_string()
}
Err(error) => serde_json::json!({ "ok": false, "message": error }).to_string(),
}
}
MeasureKind::Face => match brep_kernel::face_measurements_native(handle, &query.entity) {
Ok((area, edge_total, surface_type)) => serde_json::json!({
"ok": true,
"kind": "face",
"solid": query.owner,
"surfaceType": surface_type,
"area": area,
"edgeLengthTotal": edge_total,
})
.to_string(),
Err(error) => serde_json::json!({ "ok": false, "message": error }).to_string(),
},
MeasureKind::Edge => match brep_kernel::edge_length_native(handle, &query.entity) {
Ok(length) => serde_json::json!({
"ok": true,
"kind": "edge",
"solid": query.owner,
"length": length,
})
.to_string(),
Err(error) => serde_json::json!({ "ok": false, "message": error }).to_string(),
},
}
}
pub struct InlineRunner {
runner: crate::pipeline::SceneRunner,
pending: std::collections::VecDeque<RunReply>,
query_pending: std::collections::VecDeque<MeasureReply>,
mesh_import_pending: std::collections::VecDeque<MeshImportReply>,
}
impl InlineRunner {
pub fn new() -> Self {
Self {
runner: crate::pipeline::SceneRunner::new(),
pending: std::collections::VecDeque::new(),
query_pending: std::collections::VecDeque::new(),
mesh_import_pending: std::collections::VecDeque::new(),
}
}
}
impl HistoryRunner for InlineRunner {
fn submit_run(&mut self, request: brep_kernel::HistoryRequest, generation: u64) {
let output = self.runner.run(&request, None);
self.pending.push_back(RunReply { generation, output });
}
fn sync_parts_library(
&mut self,
_revision: u64,
_fetch: &mut dyn FnMut() -> brep_kernel::PartsLibraryMap,
) {
}
fn poll_run(&mut self) -> Option<RunReply> {
self.pending.pop_front()
}
fn submit_query(&mut self, query: MeasureQuery) {
let result = measure_json(&self.runner, &query);
self.query_pending.push_back(MeasureReply { id: query.id, result });
}
fn poll_query(&mut self) -> Option<MeasureReply> {
self.query_pending.pop_front()
}
fn submit_mesh_import(&mut self, request: MeshImportRequest) {
self.mesh_import_pending
.push_back(reconstruct_mesh(request));
}
fn poll_mesh_import(&mut self) -> Option<MeshImportReply> {
self.mesh_import_pending.pop_front()
}
fn reset(&mut self) {
self.runner.reset();
}
}
impl Default for InlineRunner {
fn default() -> Self {
Self::new()
}
}
#[derive(serde::Serialize, serde::Deserialize)]
pub enum Command {
Run {
request: brep_kernel::HistoryRequest,
generation: u64,
#[serde(default)]
parts_library_revision: u64,
},
SetPartsLibrary {
library: brep_kernel::PartsLibraryMap,
revision: u64,
},
Query(MeasureQuery),
MeshImport(MeshImportRequest),
Reset,
}
#[derive(serde::Serialize, serde::Deserialize)]
pub enum Reply {
Run(RunReply),
Query(MeasureReply),
MeshImport(MeshImportReply),
NeedPartsLibrary,
}
fn reconstruct_mesh(request: MeshImportRequest) -> MeshImportReply {
let result = (|| {
use brep_reconstruction::stl_conversion::{
binary_stl_coordinate_precision_tolerance, convert_stl_mesh_to_step,
StlConversionOptions,
};
use brep_reconstruction::{Mesh, Vec3};
let (mesh, positions, indices, coordinate_precision_tolerance) = match request.format {
MeshImportFormat::Stl => {
use brep_reconstruction::stl::{parse_stl_bytes, StlFormat, StlReadOptions};
let imported = parse_stl_bytes(&request.bytes, &StlReadOptions::default())
.map_err(|error| format!("STL import failed: {error}"))?;
let positions = imported
.mesh
.vertices
.iter()
.flat_map(|point| [point.x, point.y, point.z])
.collect::<Vec<_>>();
let indices = imported
.mesh
.triangles
.iter()
.flatten()
.copied()
.collect::<Vec<_>>();
let precision = if imported.format == StlFormat::Binary {
binary_stl_coordinate_precision_tolerance(&imported.mesh)
} else {
0.0
};
(imported.mesh, positions, indices, precision)
}
MeshImportFormat::Obj => {
let text = std::str::from_utf8(&request.bytes)
.map_err(|_| "OBJ import failed: file is not UTF-8 text".to_string())?;
let obj = brep_kernel::read_obj(text)
.map_err(|error| format!("OBJ import failed: {error}"))?;
let vertices = obj
.positions
.chunks_exact(3)
.map(|point| Vec3::new(point[0], point[1], point[2]))
.collect::<Vec<_>>();
let triangles = obj
.indices
.chunks_exact(3)
.map(|triangle| [triangle[0], triangle[1], triangle[2]])
.collect::<Vec<_>>();
(
Mesh::new(vertices, triangles),
obj.positions,
obj.indices,
0.0,
)
}
};
let mut options = StlConversionOptions::default();
options.coordinate_precision_tolerance = coordinate_precision_tolerance;
convert_stl_mesh_to_step(
&mesh,
&positions,
Some(&indices),
&options,
"Imported mesh",
"MM",
"",
)
.map(|output| output.step_text)
.map_err(|error| format!("RANSAC reconstruction failed: {error}"))
})();
MeshImportReply {
id: request.id,
result,
}
}
pub fn process_command(
runner: &mut crate::pipeline::SceneRunner,
command: Command,
) -> Option<Reply> {
match command {
Command::Run {
request,
generation,
parts_library_revision,
} => {
let stale = match runner.parts_library_revision {
Some(installed) => installed != parts_library_revision,
None => parts_library_revision != 0,
};
let recoverable = brep_kernel::missing_library_parts(&request)
.iter()
.any(|name| runner.parts_library_names.contains(name));
if stale || recoverable {
runner.parts_library_revision = None;
return Some(Reply::NeedPartsLibrary);
}
let output = runner.run(&request, None);
Some(Reply::Run(RunReply { generation, output }))
}
Command::SetPartsLibrary { library, revision } => {
runner.parts_library_names = library.keys().cloned().collect();
brep_kernel::install_parts_library(&library);
runner.parts_library_revision = Some(revision);
None
}
Command::Query(query) => {
let result = measure_json(runner, &query);
Some(Reply::Query(MeasureReply { id: query.id, result }))
}
Command::MeshImport(request) => Some(Reply::MeshImport(reconstruct_mesh(request))),
Command::Reset => {
runner.reset();
brep_kernel::clear_history_cache();
None
}
}
}
#[cfg(not(target_arch = "wasm32"))]
pub struct ThreadRunner {
tx: Option<std::sync::mpsc::Sender<Command>>,
rx: std::sync::mpsc::Receiver<Reply>,
handle: Option<std::thread::JoinHandle<()>>,
run_buf: std::collections::VecDeque<RunReply>,
query_buf: std::collections::VecDeque<MeasureReply>,
mesh_import_buf: std::collections::VecDeque<MeshImportReply>,
sent_library_revision: Option<u64>,
library_requested: bool,
}
#[cfg(not(target_arch = "wasm32"))]
impl ThreadRunner {
pub fn new() -> Self {
let (tx, cmd_rx) = std::sync::mpsc::channel::<Command>();
let (reply_tx, rx) = std::sync::mpsc::channel::<Reply>();
let handle = std::thread::Builder::new()
.name("brep-history-runner".to_string())
.spawn(move || thread_main(cmd_rx, reply_tx))
.expect("spawn brep-history-runner thread");
Self {
tx: Some(tx),
rx,
handle: Some(handle),
run_buf: std::collections::VecDeque::new(),
query_buf: std::collections::VecDeque::new(),
mesh_import_buf: std::collections::VecDeque::new(),
sent_library_revision: None,
library_requested: false,
}
}
fn drain(&mut self) {
while let Ok(reply) = self.rx.try_recv() {
match reply {
Reply::Run(run) => self.run_buf.push_back(run),
Reply::Query(query) => self.query_buf.push_back(query),
Reply::MeshImport(reply) => self.mesh_import_buf.push_back(reply),
Reply::NeedPartsLibrary => {
self.sent_library_revision = None;
self.library_requested = true;
}
}
}
}
}
#[cfg(not(target_arch = "wasm32"))]
impl Default for ThreadRunner {
fn default() -> Self {
Self::new()
}
}
#[cfg(not(target_arch = "wasm32"))]
fn thread_main(
cmd_rx: std::sync::mpsc::Receiver<Command>,
reply_tx: std::sync::mpsc::Sender<Reply>,
) {
let mut runner = crate::pipeline::SceneRunner::new();
while let Ok(first) = cmd_rx.recv() {
let mut batch = vec![first];
loop {
match cmd_rx.try_recv() {
Ok(command) => batch.push(command),
Err(_) => break, }
}
let mut run_here: Vec<bool> = vec![true; batch.len()];
for i in 0..batch.len() {
if matches!(batch[i], Command::Run { .. })
&& matches!(batch.get(i + 1), Some(Command::Run { .. }))
{
run_here[i] = false;
}
}
for (i, command) in batch.into_iter().enumerate() {
if !run_here[i] {
continue;
}
if let Some(reply) = process_command(&mut runner, command) {
if reply_tx.send(reply).is_err() {
return; }
}
}
}
}
#[cfg(not(target_arch = "wasm32"))]
impl HistoryRunner for ThreadRunner {
fn submit_run(&mut self, request: brep_kernel::HistoryRequest, generation: u64) {
if let Some(tx) = &self.tx {
let _ = tx.send(Command::Run {
request,
generation,
parts_library_revision: self.sent_library_revision.unwrap_or(0),
});
}
}
fn sync_parts_library(
&mut self,
revision: u64,
fetch: &mut dyn FnMut() -> brep_kernel::PartsLibraryMap,
) {
if self.sent_library_revision == Some(revision) {
return;
}
if let Some(tx) = &self.tx {
let _ = tx.send(Command::SetPartsLibrary {
library: fetch(),
revision,
});
self.sent_library_revision = Some(revision);
}
}
fn poll_library_request(&mut self) -> bool {
self.drain();
std::mem::take(&mut self.library_requested)
}
fn poll_run(&mut self) -> Option<RunReply> {
self.drain();
self.run_buf.pop_front()
}
fn submit_query(&mut self, query: MeasureQuery) {
if let Some(tx) = &self.tx {
let _ = tx.send(Command::Query(query));
}
}
fn poll_query(&mut self) -> Option<MeasureReply> {
self.drain();
self.query_buf.pop_front()
}
fn submit_mesh_import(&mut self, request: MeshImportRequest) {
if let Some(tx) = &self.tx {
let _ = tx.send(Command::MeshImport(request));
}
}
fn poll_mesh_import(&mut self) -> Option<MeshImportReply> {
self.drain();
self.mesh_import_buf.pop_front()
}
fn reset(&mut self) {
if let Some(tx) = &self.tx {
let _ = tx.send(Command::Reset);
}
self.run_buf.clear();
self.query_buf.clear();
self.mesh_import_buf.clear();
self.sent_library_revision = None;
self.library_requested = false;
}
}
#[cfg(not(target_arch = "wasm32"))]
impl Drop for ThreadRunner {
fn drop(&mut self) {
self.tx.take();
if let Some(handle) = self.handle.take() {
let _ = handle.join();
}
}
}
#[cfg(all(test, not(target_arch = "wasm32")))]
mod thread_tests {
use super::*;
fn box_request() -> brep_kernel::HistoryRequest {
serde_json::from_str(
r#"{
"expressions": "", "configurator": {},
"features": [{
"type": "P.CU",
"inputParams": {
"id": "Box", "sizeX": 20.0, "sizeY": 20.0, "sizeZ": 20.0,
"transform": { "position": [0,0,0], "rotationEuler": [0,0,0], "scale": [1,1,1] },
"boolean": { "targets": [], "operation": "NONE" }
},
"persistentData": {}
}]
}"#,
)
.unwrap()
}
fn spin_run(runner: &mut ThreadRunner) -> RunReply {
for _ in 0..3000 {
if let Some(reply) = runner.poll_run() {
return reply;
}
std::thread::sleep(std::time::Duration::from_millis(1));
}
panic!("thread run did not complete within the spin budget");
}
fn spin_query(runner: &mut ThreadRunner) -> MeasureReply {
for _ in 0..3000 {
if let Some(reply) = runner.poll_query() {
return reply;
}
std::thread::sleep(std::time::Duration::from_millis(1));
}
panic!("thread query did not complete within the spin budget");
}
#[test]
fn thread_runner_runs_and_replies() {
let mut runner = ThreadRunner::new();
runner.submit_run(box_request(), 1);
let reply = spin_run(&mut runner);
assert_eq!(reply.generation, 1, "reply carries its submit generation");
assert_eq!(reply.output.snapshot.len(), 1, "one solid (the box)");
let (name, _handle, display) = &reply.output.snapshot[0];
assert_eq!(name, "Box");
assert!(display.is_some(), "first emit is a fresh tessellation, not a reuse");
assert_eq!(reply.output.provenance, vec![("Box".to_string(), "Box".to_string())]);
}
#[test]
fn thread_runner_measures_after_run() {
let mut runner = ThreadRunner::new();
runner.submit_run(box_request(), 1);
let _ = spin_run(&mut runner);
runner.submit_query(MeasureQuery {
id: 7,
kind: MeasureKind::Solid,
owner: "Box".to_string(),
entity: String::new(),
density: 1.0,
});
let reply = spin_query(&mut runner);
assert_eq!(reply.id, 7);
let info: serde_json::Value = serde_json::from_str(&reply.result).unwrap();
assert_eq!(info["ok"], true, "measured on the thread: {}", reply.result);
assert!(
(info["volume"].as_f64().unwrap() - 8000.0).abs() < 1.0,
"box volume {} != 8000",
info["volume"]
);
}
#[test]
fn thread_runner_reuses_on_identical_resubmit() {
let mut runner = ThreadRunner::new();
runner.submit_run(box_request(), 1);
let first = spin_run(&mut runner);
assert!(first.output.snapshot[0].2.is_some(), "first is fresh");
runner.submit_run(box_request(), 2);
let second = spin_run(&mut runner);
assert_eq!(second.generation, 2);
assert!(
second.output.snapshot[0].2.is_none(),
"identical resubmit replays as a REUSE (unchanged handle)"
);
}
fn widget_part() -> (brep_kernel::PartsLibraryMap, brep_kernel::HistoryRequest) {
let document = serde_json::json!({
"expressions": "", "configurator": {},
"features": [{
"type": "P.CU",
"inputParams": {
"id": "Part", "sizeX": 4.0, "sizeY": 4.0, "sizeZ": 4.0,
"transform": { "position": [0,0,0], "rotationEuler": [0,0,0], "scale": [1,1,1] },
"boolean": { "targets": [], "operation": "NONE" }
},
"persistentData": {}
}]
});
let mut library = brep_kernel::PartsLibraryMap::new();
library.insert(
"widget".to_string(),
brep_kernel::PartsLibraryEntry {
document,
..Default::default()
},
);
let request = serde_json::from_str(
r#"{
"expressions": "", "configurator": {},
"features": [{
"type": "ACOMP",
"inputParams": {
"id": "ACOMP1", "partName": "widget",
"transform": { "position": [0,0,0], "rotationEuler": [0,0,0], "scale": [1,1,1] }
},
"persistentData": {}
}]
}"#,
)
.unwrap();
(library, request)
}
fn spin_reply(runner: &mut ThreadRunner) -> Option<RunReply> {
for _ in 0..3000 {
if runner.poll_library_request() {
return None;
}
if let Some(reply) = runner.poll_run() {
return Some(reply);
}
std::thread::sleep(std::time::Duration::from_millis(1));
}
panic!("thread run neither completed nor asked for the library");
}
#[test]
fn thread_runner_installs_the_library_once_and_reuses_it() {
let (library, request) = widget_part();
let mut runner = ThreadRunner::new();
let mut fetches = 0;
for generation in 1..=3 {
runner.sync_parts_library(1, &mut || {
fetches += 1;
library.clone()
});
runner.submit_run(request.clone(), generation);
let reply = spin_reply(&mut runner).expect("the run executed");
assert_eq!(reply.generation, generation);
assert_eq!(
reply.output.snapshot.len(),
1,
"the ACOMP resolved against the resident library"
);
}
assert_eq!(fetches, 1, "the library is sent once, not once per run");
}
#[test]
fn thread_runner_recovers_after_its_library_is_gc_d() {
let (library, request) = widget_part();
let empty: brep_kernel::HistoryRequest =
serde_json::from_str(r#"{"expressions":"","configurator":{},"features":[]}"#).unwrap();
let mut runner = ThreadRunner::new();
runner.sync_parts_library(1, &mut || library.clone());
runner.submit_run(request.clone(), 1);
assert!(spin_reply(&mut runner).is_some(), "first run executes");
runner.sync_parts_library(1, &mut || library.clone());
runner.submit_run(empty, 2);
assert!(spin_reply(&mut runner).is_some(), "the empty run executes");
runner.sync_parts_library(1, &mut || library.clone());
runner.submit_run(request.clone(), 3);
assert!(
spin_reply(&mut runner).is_none(),
"a run whose part the thread GC'd must be REFUSED, never run against \
an empty library"
);
let mut refetched = 0;
runner.sync_parts_library(1, &mut || {
refetched += 1;
library.clone()
});
assert_eq!(refetched, 1, "the refusal forces a re-send");
runner.submit_run(request, 4);
let reply = spin_reply(&mut runner).expect("the re-driven run executes");
assert_eq!(reply.generation, 4);
assert_eq!(reply.output.snapshot.len(), 1, "the component is back");
}
#[test]
fn a_run_stamped_for_an_unknown_library_is_refused() {
let (_library, request) = widget_part();
let mut scene = crate::pipeline::SceneRunner::new();
let refusal = process_command(
&mut scene,
Command::Run {
request,
generation: 1,
parts_library_revision: 7,
},
);
assert!(
matches!(refusal, Some(Reply::NeedPartsLibrary)),
"an unknown library stamp must refuse the run"
);
assert_eq!(
scene.parts_library_revision, None,
"the refusal drops what the runner believed it held"
);
}
#[test]
fn a_part_the_library_never_had_errors_instead_of_looping() {
let (_library, request) = widget_part();
let mut runner = ThreadRunner::new();
runner.sync_parts_library(1, &mut brep_kernel::PartsLibraryMap::new);
runner.submit_run(request, 1);
let reply = spin_reply(&mut runner)
.expect("a dangling part name must not refuse the run forever");
assert!(
!reply.output.report.feature_errors.is_empty(),
"the ACOMP reports the missing part: {:?}",
reply.output.report
);
}
#[test]
fn thread_runner_reset_forces_full_rebuild() {
let mut runner = ThreadRunner::new();
runner.submit_run(box_request(), 1);
let _ = spin_run(&mut runner);
runner.reset();
runner.submit_run(box_request(), 2);
let reply = spin_run(&mut runner);
assert!(
reply.output.snapshot[0].2.is_some(),
"after reset the box re-tessellates (baseline dropped)"
);
}
#[test]
fn run_command_round_trips_through_serde() {
let command = Command::Run {
request: box_request(),
generation: 42,
parts_library_revision: 9,
};
let json = serde_json::to_string(&command).expect("Command serializes");
let back: Command = serde_json::from_str(&json).expect("Command deserializes");
match back {
Command::Run {
request,
generation,
parts_library_revision,
} => {
assert_eq!(generation, 42, "generation round-trips");
assert_eq!(parts_library_revision, 9, "library stamp round-trips");
assert_eq!(request.features.len(), 1);
assert_eq!(request.features[0].feature_type, "P.CU");
let output = crate::pipeline::SceneRunner::new().run(&request, None);
assert_eq!(output.snapshot.len(), 1);
assert_eq!(output.snapshot[0].0, "Box");
}
_ => panic!("round-tripped to the wrong Command variant"),
}
}
#[test]
fn mesh_import_command_round_trips_through_serde() {
let command = Command::MeshImport(MeshImportRequest {
id: 17,
format: MeshImportFormat::Stl,
bytes: vec![0, 1, 127, 128, 255],
});
let json = serde_json::to_string(&command).expect("mesh command serializes");
let back: Command = serde_json::from_str(&json).expect("mesh command deserializes");
match back {
Command::MeshImport(request) => {
assert_eq!(request.id, 17);
assert!(matches!(request.format, MeshImportFormat::Stl));
assert_eq!(request.bytes, vec![0, 1, 127, 128, 255]);
}
_ => panic!("round-tripped to the wrong Command variant"),
}
}
}