Skip to main content

geop_ops_model/
draw.rs

1//! The model as the viewport draws it: its vertices, edges and faces, each
2//! by name, drawn into a [`PartView`] from its rasterization.
3
4use std::{cell::RefCell, collections::HashMap};
5
6use geop_core_editor::{
7    registry::{Drawer, StructureItem},
8    view::{FaceMesh, PartView, RESOLUTION, ViewEdge, ViewFace, ViewVertex},
9};
10use geop_core_geometry::search::PROJECT_ITERATIONS;
11use geop_core_math::{geop_error::GeopResult, scalars::Scalar, vector::Vector3};
12use geop_core_program::reader::Reader;
13use geop_core_rasterize::Rasterizer;
14use geop_core_topology::{CoedgeId, EdgeId, Model, SolidId};
15
16use crate::{NamedModel, RefId, read_model};
17
18/// The solid `edge` bounds: that of a face it runs along.
19fn solid_of_edge(model: &Model, edge: EdgeId) -> Option<SolidId> {
20    let coedge = *model.coedges_of_edge(edge).first()?;
21    model
22        .solid_of_face(model.get_coedge(coedge).ok()?.face)
23        .ok()?
24}
25
26impl NamedModel {
27    /// Draws its vertices, edges and faces, and lists its solids, into
28    /// `view`, as elements of the target `target`: its faces meshed by
29    /// `rasterizer`, which meshes again only those it has not meshed alike.
30    pub fn draw(
31        &self,
32        target: &str,
33        rasterizer: &mut Rasterizer,
34        view: &mut PartView,
35    ) -> GeopResult<()> {
36        let model = self.topology();
37        let raster = rasterizer.rasterize(model, RESOLUTION)?;
38        let name = |id: RefId| self.name_of(id).unwrap_or_default().to_string();
39
40        // A vertex is a corner of the solid an edge at it bounds, and of the
41        // faces those edges bound.
42        let mut corner_of = HashMap::new();
43        let mut faces_of_edge = HashMap::new();
44        let mut faces_at_corner: HashMap<_, Vec<String>> = HashMap::new();
45        for (&id, edge) in &model.edges {
46            if let Some(solid) = solid_of_edge(model, id) {
47                corner_of.insert(edge.start_vertex, solid);
48                corner_of.insert(edge.end_vertex, solid);
49            }
50            let mut faces: Vec<String> = (model.coedges_of_edge(id).into_iter())
51                .filter_map(|c| model.get_coedge(c).ok())
52                .map(|c| name(c.face.into()))
53                .collect();
54            faces.sort();
55            faces.dedup();
56            for v in [edge.start_vertex, edge.end_vertex] {
57                let at = faces_at_corner.entry(v).or_default();
58                at.extend(faces.iter().cloned());
59                at.sort();
60                at.dedup();
61            }
62            faces_of_edge.insert(id, faces);
63        }
64        let features = self.feature_faces();
65        view.model = target.to_string();
66        // Every map here is ordered by id, so these come in id order.
67        view.vertices = (raster.vertices.iter())
68            .map(|(&id, p)| ViewVertex {
69                name: name(id.into()),
70                solid: corner_of.get(&id).map(|&s| name(s.into())),
71                faces: faces_at_corner.get(&id).cloned().unwrap_or_default(),
72                at: *p,
73            })
74            .collect();
75        view.edges = (raster.edges.iter())
76            .map(|(&id, polyline)| ViewEdge {
77                ends: (model.get_edge(id))
78                    .map(|e| [e.start_vertex, e.end_vertex].map(|v| name(v.into())))
79                    .unwrap_or_default(),
80                solid: solid_of_edge(model, id).map(|s| name(s.into())),
81                faces: faces_of_edge.get(&id).cloned().unwrap_or_default(),
82                name: name(id.into()),
83                polyline: polyline.clone(),
84                within_a_surface: within_a_surface(model, id),
85            })
86            .collect();
87        view.faces = (raster.faces.iter())
88            .map(|(&id, tris)| {
89                let face = name(id.into());
90                ViewFace {
91                    feature: features.of(&face).map(str::to_string),
92                    also: self.names.aliases_of(&face),
93                    name: face,
94                    solid: (model.solid_of_face(id).ok().flatten()).map(|s| name(s.into())),
95                    mesh: FaceMesh::new(
96                        tris.iter().map(|t| [t.a, t.b, t.c]).collect(),
97                        (tris.iter())
98                            .map(|t| t.vertex_normals.unwrap_or([t.normal; 3]))
99                            .collect(),
100                    ),
101                }
102            })
103            .collect();
104        view.solids = model.solids.keys().map(|&s| name(s.into())).collect();
105        Ok(())
106    }
107}
108
109/// Draws a part's model, as the target `model`, and lists its solids and
110/// the faces standing on their own, of no solid. Its faces are meshed by
111/// one [`Rasterizer`]: a part drawn with only some faces changed — the part
112/// before a step opened — meshes those again, not all.
113#[derive(Default)]
114pub struct ModelDrawer {
115    rasterizer: RefCell<Rasterizer>,
116}
117
118impl Drawer for ModelDrawer {
119    fn draw(&self, reader: &Reader, view: &mut PartView) -> GeopResult<()> {
120        read_model(reader)?.draw("model", &mut self.rasterizer.borrow_mut(), view)
121    }
122
123    fn structure(&self, reader: &Reader) -> GeopResult<Vec<StructureItem>> {
124        let model = read_model(reader)?;
125        let solids =
126            (model.solid_names().into_iter()).map(|n| StructureItem::drawn("solid", "Solids", &n));
127        let faces = (model.sheet_face_names().into_iter())
128            .map(|n| StructureItem::drawn("face", "Faces", &n));
129        Ok(solids.chain(faces).collect())
130    }
131
132    /// Meshing the faces takes work: what it drew is kept.
133    fn kept(&self) -> bool {
134        true
135    }
136}
137
138/// How far apart, as the sine of the angle, the faces either side of an edge
139/// may lie for it to show no crease, and how far apart their curvatures across
140/// it, as how much further one turns than the other over the larger face, in
141/// radians. A helix's flank jumps in curvature at each quarter turn, by 0.13
142/// radians over the quarter on a square thread; a fillet turns through its
143/// whole angle. What they decide is only what is drawn: a blend tilted a
144/// millionth of a radian off its face is smooth to the eye, and to no other
145/// question.
146const CREASE: f64 = 0.0175;
147const TURN: f64 = 0.25;
148
149/// Whether `edge` lies within one smooth surface, its faces pieces of it: they
150/// are tangent where looked at, and curve nearly alike across it. So a cylinder's
151/// seam has no line, and where a fillet leaves a flat face, tangent to it but
152/// curving where the face does not, keeps one. A line is drawn by samples
153/// anyway, so that is what this asks of too; an edge it cannot tell about is
154/// drawn.
155fn within_a_surface(model: &Model, edge: EdgeId) -> bool {
156    let holds = || -> GeopResult<bool> {
157        let coedges = model.coedges_of_edge(edge);
158        let [a, b] = coedges.as_slice() else {
159            return Ok(false);
160        };
161        let curve = &model.get_edge(edge)?.curve;
162        let (t0, t1) = curve.domain();
163        let size = |coedge: CoedgeId| -> GeopResult<f64> {
164            let boxed = model
165                .get_face(model.get_coedge(coedge)?.face)?
166                .surface
167                .aabb();
168            Ok(boxed
169                .iter()
170                .map(|x| x.width().to_f64().powi(2))
171                .sum::<f64>()
172                .sqrt())
173        };
174        let across_faces = size(*a)?.max(size(*b)?);
175        // The face's surface at `point`: its unit normal, and how it curves
176        // towards the side the edge does not run along.
177        let side =
178            |coedge: CoedgeId, point: Vector3, along: &Vector3| -> GeopResult<(Vector3, f64)> {
179                let coedge = model.get_coedge(coedge)?;
180                let surface = &model.get_face(coedge.face)?.surface;
181                let seed = coedge.pcurve.evaluate(coedge.pcurve.mid_parameter()?)?;
182                let (u, v) = surface.project(point, seed[0], seed[1], PROJECT_ITERATIONS)?;
183                let normal = surface.normal(u, v)?;
184                let across = normal.prod_cross(along);
185                Ok((normal, surface.normal_curvature(u, v, &across)?.to_f64()))
186            };
187        for fraction in [0.25, 0.5, 0.75] {
188            let t = Scalar::interpolate(t0, t1, Scalar::from_f64(fraction)).sharpen();
189            let point = curve.evaluate(t)?;
190            let along = curve.tangent(t)?.normalize()?;
191            let ((n, k), (m, l)) = (side(*a, point, &along)?, side(*b, point, &along)?);
192            let turned = n.prod_cross(&m).norm().to_f64();
193            if turned > CREASE {
194                return Ok(false);
195            }
196            // Curvature is signed by the normal, and the two may point apart.
197            let l = if n.prod_dot(&m).to_f64() < 0.0 { -l } else { l };
198            if (k - l).abs() * across_faces > TURN {
199                return Ok(false);
200            }
201        }
202        Ok(true)
203    };
204    holds().unwrap_or(false)
205}
206
207#[cfg(test)]
208mod tests {
209    use geop_core_editor::view::drawn;
210    use geop_core_editor::{
211        editing::{Pointer, Reach},
212        selectable::selected,
213    };
214    use geop_core_math::{primitives::Ray, scalars::Scalar, vector::Vector3};
215    use geop_core_program::{element::ElementReference, part::Part};
216
217    use crate::{
218        Body, Line, NamedModel, model,
219        testing::{given_part, unit_cube},
220    };
221
222    use super::*;
223
224    /// Looking down at `(x, y)` from above the unit cube.
225    fn from_above(x: f64, y: f64) -> Pointer {
226        let v = |p: [f64; 3]| Vector3::from_array(p.map(Scalar::from_f64));
227        Pointer {
228            cycle: 0,
229            ray: Ray::try_new(v([x, y, 10.0]), v([0.0, 0.0, -1.0])).unwrap(),
230            reach: Reach::Tube {
231                radius: Scalar::from_f64(0.01),
232            },
233        }
234    }
235
236    fn accepts(kinds: &[&str]) -> Vec<String> {
237        kinds.iter().map(|k| k.to_string()).collect()
238    }
239
240    /// Looking down at the cube, a corner is a vertex, a side's middle an
241    /// edge and the middle of the top a face — of the target it was drawn
242    /// as. What a field does not accept is passed over for what it does.
243    #[test]
244    fn a_pick_hits_the_smallest_element_under_the_pointer() {
245        let mut view = PartView::new();
246        unit_cube()
247            .draw("model", &mut Rasterizer::default(), &mut view)
248            .unwrap();
249        let view = view.finish();
250        assert_eq!(
251            (view.vertices.len(), view.edges.len(), view.faces.len()),
252            (8, 12, 6)
253        );
254        let all = accepts(&[drawn::VERTEX, drawn::EDGE, drawn::FACE]);
255        let hit =
256            |x, y, accepts: &[String]| view.pick(&from_above(x, y), accepts, &[]).unwrap().element;
257        assert_eq!(hit(1.0, 1.0, &all).kind, drawn::VERTEX);
258        assert_eq!(hit(0.5, 1.0, &all).kind, drawn::EDGE);
259        let top = hit(0.5, 0.5, &all);
260        assert_eq!(
261            (top.kind.as_str(), top.target.to_string().as_str()),
262            (drawn::FACE, "/:model")
263        );
264        assert_eq!(hit(1.0, 1.0, &accepts(&[drawn::FACE])).kind, drawn::FACE);
265        assert!(view.pick(&from_above(5.0, 5.0), &all, &[]).is_none());
266    }
267
268    /// What a field asks for is selected from the pick: a line from an
269    /// edge, the solid as a body from its face.
270    #[test]
271    fn a_pick_selects_what_a_field_asks_for() {
272        let mut view = PartView::new();
273        unit_cube()
274            .draw("model", &mut Rasterizer::default(), &mut view)
275            .unwrap();
276        let view = view.finish();
277        let part: Part = given_part(&unit_cube()).unwrap();
278        let reader = part.current();
279        let edge = view
280            .pick(&from_above(0.5, 1.0), &accepts(&[drawn::EDGE]), &[])
281            .unwrap();
282        let lines: Vec<ElementReference<NamedModel, Line>> =
283            selected(&reader, std::slice::from_ref(&edge.element));
284        assert_eq!(lines[0].name(), edge.element.name);
285        let face = view
286            .pick(&from_above(0.5, 0.5), &accepts(&[drawn::FACE]), &[])
287            .unwrap();
288        let bodies: Vec<ElementReference<NamedModel, Body>> = selected(&reader, &[face.element]);
289        assert_eq!(bodies, [ElementReference::new(model(), "s0")]);
290    }
291
292    /// A face is hit where the ray enters the part; a solid on any of its
293    /// faces; and nothing where the ray misses. Drawn as an editor draws a
294    /// part, by the drawers `register` adds.
295    #[test]
296    fn faces_and_solids() {
297        use geop_core_editor::{registry::Registry, selectable};
298
299        use crate::Solid;
300        let part: Part = given_part(&unit_cube()).unwrap();
301        let registry = crate::register(Registry::default()).unwrap();
302        let view = (registry.draw(&part.current(), &Default::default(), None)).unwrap();
303        let faces = accepts(&[drawn::FACE]);
304        let hit = view.pick(&from_above(0.5, 0.5), &faces, &[]).unwrap();
305        assert_eq!(hit.element.kind, drawn::FACE);
306        assert!(hit.point[2].could_be_equal(Scalar::ONE), "{hit:?}");
307        let solid = selectable::pick::<ElementReference<NamedModel, Solid>>(
308            &view,
309            &part.current(),
310            &from_above(0.5, 0.5),
311        );
312        assert!(solid.is_some());
313        assert!(view.pick(&from_above(5.0, 5.0), &faces, &[]).is_none());
314    }
315}