fj_host/
host_thread.rs

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