1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147
use std::thread::{self, JoinHandle};
use crossbeam_channel::{self, Receiver, Sender};
use fj_interop::processed_shape::ProcessedShape;
use fj_operations::shape_processor::ShapeProcessor;
use crate::{Error, HostCommand, Model, Watcher};
// Use a zero-sized error type to silence `#[warn(clippy::result_large_err)]`.
// The only error from `EventLoopProxy::send_event` is `EventLoopClosed<T>`,
// so we don't need the actual value. We just need to know there was an error.
pub(crate) struct EventLoopClosed;
pub(crate) struct HostThread {
shape_processor: ShapeProcessor,
model_event_tx: Sender<ModelEvent>,
command_tx: Sender<HostCommand>,
command_rx: Receiver<HostCommand>,
}
impl HostThread {
// Spawn a background thread that will process models for an event loop.
pub(crate) fn spawn(
shape_processor: ShapeProcessor,
event_loop_proxy: Sender<ModelEvent>,
) -> (Sender<HostCommand>, JoinHandle<Result<(), EventLoopClosed>>) {
let (command_tx, command_rx) = crossbeam_channel::unbounded();
let command_tx_2 = command_tx.clone();
let host_thread = Self {
shape_processor,
model_event_tx: event_loop_proxy,
command_tx,
command_rx,
};
let join_handle = host_thread.spawn_thread();
(command_tx_2, join_handle)
}
fn spawn_thread(mut self) -> JoinHandle<Result<(), EventLoopClosed>> {
thread::Builder::new()
.name("host".to_string())
.spawn(move || -> Result<(), EventLoopClosed> {
let mut model: Option<Model> = None;
let mut _watcher: Option<Watcher> = None;
while let Ok(command) = self.command_rx.recv() {
match command {
HostCommand::LoadModel(new_model) => {
// Right now, `fj-app` will only load a new model
// once. The gui does not have a feature to load a
// new model after the initial load. If that were
// to change, there would be a race condition here
// if the prior watcher sent `TriggerEvaluation`
// before it and the model were replaced.
match Watcher::watch_model(
new_model.watch_path(),
self.command_tx.clone(),
) {
Ok(watcher) => {
_watcher = Some(watcher);
self.send_event(ModelEvent::StartWatching)?;
}
Err(err) => {
self.send_event(ModelEvent::Error(err))?;
continue;
}
}
self.process_model(&new_model)?;
model = Some(new_model);
}
HostCommand::TriggerEvaluation => {
self.send_event(ModelEvent::ChangeDetected)?;
if let Some(model) = &model {
self.process_model(model)?;
}
}
}
}
Ok(())
})
.expect("Cannot create OS thread for host")
}
// Evaluate and process a model.
fn process_model(&mut self, model: &Model) -> Result<(), EventLoopClosed> {
let evaluation = match model.evaluate() {
Ok(evaluation) => evaluation,
Err(err) => {
self.send_event(ModelEvent::Error(err))?;
return Ok(());
}
};
self.send_event(ModelEvent::Evaluated)?;
if let Some(warn) = evaluation.warning {
self.send_event(ModelEvent::Warning(warn))?;
}
match self.shape_processor.process(&evaluation.shape) {
Ok(shape) => self.send_event(ModelEvent::ProcessedShape(shape))?,
Err(err) => {
self.send_event(ModelEvent::Error(err.into()))?;
}
}
Ok(())
}
// Send a message to the event loop.
fn send_event(&mut self, event: ModelEvent) -> Result<(), EventLoopClosed> {
self.model_event_tx
.send(event)
.map_err(|_| EventLoopClosed)?;
Ok(())
}
}
/// An event emitted by the host thread
#[derive(Debug)]
pub enum ModelEvent {
/// A new model is being watched
StartWatching,
/// A change in the model has been detected
ChangeDetected,
/// The model has been evaluated
Evaluated,
/// The model has been processed
ProcessedShape(ProcessedShape),
/// A warning
Warning(String),
/// An error
Error(Error),
}