1use crate::{Scope, scope::Stopped};
5use geop_core_editor::output::{Answer, Block, Bounded, Row};
6use geop_core_editor::{editing::Tone, selectable::SelectableElement};
7use geop_core_math::{geop_error::GeopResult, scalars::Scalar};
8use geop_core_program::{
9 element::{ElementReference, PickedElement},
10 reader::Reader,
11 watch,
12};
13use geop_core_topology::mass::MassProperties;
14use geop_ops_model::placed::solids::{PlacedSolid, Shapes, placed_solids};
15use geop_ops_model::{Solid, model, parameters::Material};
16use serde::Serialize;
17
18#[derive(Clone, Debug, Serialize)]
22pub struct MassSummary {
23 pub volume: Bounded,
24 pub area: Bounded,
25 pub center: Option<[Bounded; 3]>,
30 pub inertial: Option<Inertial>,
32 pub converged: bool,
35}
36
37#[derive(Clone, Debug, Serialize)]
41pub struct Inertial {
42 pub mass: Bounded,
43 pub inertia: [[Bounded; 3]; 3],
44 pub principal_moments: [Bounded; 3],
45 pub principal_axes: [[f64; 3]; 3],
48}
49
50#[derive(Clone, Copy)]
52pub(crate) enum Known {
53 Weighed(MassProperties),
55 Shape(MassProperties),
59}
60
61impl MassSummary {
62 pub(crate) fn of(known: &Known, area: (Scalar, bool)) -> GeopResult<Self> {
66 let (p, inertial) = match known {
67 Known::Weighed(p) => {
68 let principal = p.principal()?;
69 let inertial = Inertial {
70 mass: Bounded::of(p.mass),
71 inertia: p.inertia.map(|row| row.map(Bounded::of)),
72 principal_moments: principal.moments.map(Bounded::of),
73 principal_axes: principal.axes.map(|axis| axis.to_f64_array()),
74 };
75 (p, Some(inertial))
76 }
77 Known::Shape(p) => (p, None),
78 };
79 Ok(Self {
80 volume: Bounded::of(p.volume),
81 area: Bounded::of(area.0),
82 center: Some([0, 1, 2].map(|k| Bounded::of(p.center[k]))),
83 inertial,
84 converged: p.converged && area.1,
85 })
86 }
87
88 fn shown(&self) -> Block {
91 let mut rows = Vec::new();
92 if let Some(inertial) = &self.inertial {
93 rows.push(Row::labelled("Mass", vec![inertial.mass.show_mass()]));
94 }
95 rows.push(Row::labelled(
96 "Volume",
97 vec![format!("{} mm³", self.volume.show(3))],
98 ));
99 rows.push(Row::labelled(
100 "Area",
101 vec![format!("{} mm²", self.area.show(3))],
102 ));
103 if let Some(center) = &self.center {
104 let center: Vec<String> = center.iter().map(|c| c.show(3)).collect();
105 let label = match self.inertial {
106 Some(_) => "Centre",
107 None => "Centroid",
108 };
109 rows.push(Row::labelled(
110 label,
111 vec![format!("{} mm", center.join(", "))],
112 ));
113 }
114 if let Some(inertial) = &self.inertial {
115 rows.extend(inertial.shown());
116 }
117 Block::Table {
118 head: Vec::new(),
119 rows,
120 foot: Vec::new(),
121 }
122 }
123}
124
125impl Inertial {
126 fn shown(&self) -> [Row; 2] {
128 let axes = ["x", "y", "z"];
129 let inertia: Vec<String> = (axes.iter().zip(&self.inertia))
130 .map(|(axis, row)| {
131 let row: Vec<String> = row.iter().map(|c| format!("{:.5}", c.value)).collect();
132 format!("{axis}: {}", row.join(" "))
133 })
134 .collect();
135 let principal: Vec<String> = self.principal_moments.iter().map(|m| m.show(4)).collect();
136 let about: Vec<String> = (self.principal_axes.iter())
137 .map(|a| format!("({:.3}, {:.3}, {:.3})", a[0], a[1], a[2]))
138 .collect();
139 [
140 Row {
141 detail: Some("About the centre of mass, along the world's axes".into()),
142 ..Row::labelled("Inertia", vec![format!("{}\nkg·mm²", inertia.join("\n"))])
143 },
144 Row {
145 detail: Some(format!("about {}", about.join(", "))),
146 ..Row::labelled(
147 "Principal",
148 vec![format!("{}\nkg·mm²", principal.join("\n"))],
149 )
150 },
151 ]
152 }
153}
154
155#[derive(Clone, Debug, Serialize)]
157pub struct BodyMass {
158 pub name: String,
160 pub material: Option<Material>,
163 pub properties: Option<MassSummary>,
164 pub error: Option<String>,
165 #[serde(skip)]
167 pub drawn: Vec<PickedElement>,
168}
169
170#[derive(Clone, Debug, Serialize)]
172pub struct MassReport {
173 pub bodies: Vec<BodyMass>,
174 pub total: Option<MassSummary>,
178 pub stopped: Option<Stopped>,
181}
182
183impl MassReport {
184 pub fn shown(&self) -> Answer {
188 let mut blocks = Vec::new();
189 if let Some(Stopped { checked, of }) = self.stopped {
190 blocks.push(Block::Text {
191 text: format!("Stopped after {checked} of {of} solids: no total."),
192 tone: Tone::Error,
193 });
194 }
195 if self.bodies.is_empty() {
196 blocks.push(Block::Text {
197 text: "No solids.".into(),
198 tone: Tone::Hint,
199 });
200 }
201 if let Some(total) = self.total.as_ref().filter(|_| self.bodies.len() > 1) {
202 let mut shown = Vec::new();
203 if total.center.is_none() {
204 shown.push(Block::Text {
205 text: SOME_MATERIAL.into(),
206 tone: Tone::Hint,
207 });
208 }
209 shown.push(total.shown());
210 blocks.push(Block::Group {
211 title: format!("All {} solids", self.bodies.len()),
212 detail: None,
213 open: true,
214 light: Vec::new(),
215 blocks: shown,
216 });
217 }
218 for body in &self.bodies {
219 let mut shown = Vec::new();
220 if body.material.is_none() {
221 shown.push(Block::Text {
222 text: NO_MATERIAL.into(),
223 tone: Tone::Hint,
224 });
225 }
226 shown.extend(body.error.iter().map(|error| Block::Text {
227 text: error.clone(),
228 tone: Tone::Error,
229 }));
230 shown.extend(body.properties.iter().map(MassSummary::shown));
231 blocks.push(Block::Group {
232 title: body.name.clone(),
233 detail: Some(
234 body.material
235 .as_ref()
236 .map_or("None".into(), |m| m.name.clone()),
237 ),
238 open: self.bodies.len() == 1,
239 light: body.drawn.clone(),
240 blocks: shown,
241 });
242 }
243 Answer {
244 blocks,
245 plane: None,
246 }
247 }
248}
249
250const NO_MATERIAL: &str = "No material: set one under Properties to get mass and inertia.";
252
253const SOME_MATERIAL: &str =
255 "Only some solids have a material: of all of them, only their volume and area.";
256
257pub fn mass_report(reader: &Reader, scope: &Scope) -> GeopResult<MassReport> {
263 let solids = placed_solids(reader)?;
264 scope.check(&solids)?;
265 let solids: Vec<_> = (solids.into_iter())
266 .filter(|solid| scope.holds(&solid.instance))
267 .collect();
268 let of = solids.len();
269 let mut stopped = None;
270 let mut bodies = Vec::new();
271 let (mut weighed, mut shapes_only) = (Vec::new(), Vec::new());
272 let mut area = (Scalar::ZERO, true);
273 let mut complete = true;
274 let mut shapes = Shapes::default();
275 for (at, placed) in solids.into_iter().enumerate() {
276 if !watch::go_on("weighing solids", at, of) {
277 stopped = Some(Stopped { checked: at, of });
278 complete = false;
279 break;
280 }
281 let material = placed.material();
282 let computed = (placed.mass_properties(&mut shapes)).and_then(|p| {
283 let known = match p {
284 Some(p) => Known::Weighed(p),
285 None => Known::Shape(placed.shape_properties(&mut shapes)?),
286 };
287 let a = placed.model.topology().solid_area(placed.solid)?;
288 Ok((MassSummary::of(&known, a)?, known, a))
289 });
290 let (properties, error) = match computed {
291 Ok((summary, known, a)) => {
292 match known {
293 Known::Weighed(p) => weighed.push(p),
294 Known::Shape(p) => shapes_only.push(p),
295 }
296 area = (area.0.add(a.0), area.1 && a.1);
297 (Some(summary), None)
298 }
299 Err(e) => {
300 complete = false;
301 (None, Some(e.to_string()))
302 }
303 };
304 bodies.push(BodyMass {
305 drawn: drawn(&placed),
306 name: placed.name,
307 material,
308 properties,
309 error,
310 });
311 }
312 let total = match (weighed.is_empty(), shapes_only.is_empty()) {
313 _ if !complete => None,
314 (false, true) => (MassProperties::combine(&weighed)?)
315 .map(|p| MassSummary::of(&Known::Weighed(p), area))
316 .transpose()?,
317 (true, _) => (MassProperties::combine(&shapes_only)?)
318 .map(|p| MassSummary::of(&Known::Shape(p), area))
319 .transpose()?,
320 (false, false) => {
322 let all = || weighed.iter().chain(&shapes_only);
323 Some(MassSummary {
324 volume: Bounded::of(all().fold(Scalar::ZERO, |v, p| v.add(p.volume))),
325 area: Bounded::of(area.0),
326 center: None,
327 inertial: None,
328 converged: all().all(|p| p.converged) && area.1,
329 })
330 }
331 };
332 Ok(MassReport {
333 bodies,
334 total,
335 stopped,
336 })
337}
338
339pub(crate) fn drawn(solid: &PlacedSolid) -> Vec<PickedElement> {
343 let Some(own) = solid.model.name_of(solid.solid) else {
344 return Vec::new();
345 };
346 let reference = ElementReference::new(model(), own);
347 (Solid::drawn(&solid.reader, &reference).into_iter())
348 .map(|face| face.behind(&solid.instance))
349 .collect()
350}