#[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,
}
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 reset(&mut self);
}
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)) => serde_json::json!({
"ok": true,
"kind": "face",
"solid": query.owner,
"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>,
}
impl InlineRunner {
pub fn new() -> Self {
Self {
runner: crate::pipeline::SceneRunner::new(),
pending: std::collections::VecDeque::new(),
query_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 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 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,
},
Query(MeasureQuery),
Reset,
}
#[derive(serde::Serialize, serde::Deserialize)]
pub enum Reply {
Run(RunReply),
Query(MeasureReply),
}
pub fn process_command(
runner: &mut crate::pipeline::SceneRunner,
command: Command,
) -> Option<Reply> {
match command {
Command::Run { request, generation } => {
let output = runner.run(&request, None);
Some(Reply::Run(RunReply { generation, output }))
}
Command::Query(query) => {
let result = measure_json(runner, &query);
Some(Reply::Query(MeasureReply { id: query.id, result }))
}
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>,
}
#[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(),
}
}
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),
}
}
}
}
#[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 });
}
}
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 reset(&mut self) {
if let Some(tx) = &self.tx {
let _ = tx.send(Command::Reset);
}
self.run_buf.clear();
self.query_buf.clear();
}
}
#[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)"
);
}
#[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 };
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 } => {
assert_eq!(generation, 42, "generation 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"),
}
}
}