Skip to main content

geop_ops_model/sketch/
mod.rs

1//! The sketch target: [`PlacedSketch`], a sketch and the plane it was placed
2//! on ([`SketchPlane`]) — the value of a sketch's target ([`sketch`]) — and
3//! what it is to the steps that build on it: as a whole, its profile or a path ([`PlacedSketch`], [`Path`]),
4//! its plane ([`Plane`]), and its points and curves, as what they are
5//! ([`Point`], [`Line`], [`Circle`], [`Round`], [`Curve`]). It is drawn
6//! into the viewport as itself ([`PlacedSketch::draw`]), and each of those
7//! is picked from what is drawn of it.
8//!
9//! What a sketch's curves are named after, when they are built into the
10//! model, is the sketch and its element ids: a sketch curve `c3` of sketch
11//! `K` is `K,c3`, its ends `K,p1` after the points there (see
12//! [`piece_names`]).
13//!
14//! The step that draws a sketch is `geop_ops_sketch`'s; what reads one —
15//! an extrusion, a hole, a pattern — needs only this. [`figures`] draws the
16//! sketches examples and tests write down; [`fields`] are the building
17//! blocks of the forms of what is drawn in a sketch.
18
19#[cfg(test)]
20mod draw_tests;
21pub mod fields;
22pub mod figures;
23
24use crate::{
25    Chain, Circle, Curve, Line, NamedModel, Path, Plane, Point, Profile, Round, datums::Datums,
26};
27use geop_core_editor::{
28    one_of,
29    registry::{Drawer, StructureItem},
30    selectable::SelectableElement,
31    view::{PartView, ViewCurve, ViewSketch, ViewSketchPoint, drawn},
32};
33use geop_core_geometry::{
34    nurb_curve::{NurbCurve, NurbCurve3D},
35    shape::{Arc, Axis},
36};
37use geop_core_math::{
38    geop_error::{GeopError, GeopResult, WithContext},
39    primitives::CoordinateSystem,
40    vector::Vector3,
41    with_context,
42};
43use geop_core_program::{
44    element::{ElementReference, HasElements, Kind, PickedElement, whole},
45    reader::Reader,
46    target::{Target, TargetReference},
47};
48use geop_core_sketch::{
49    CurveId, Enclosure, PointId, Shape,
50    profile::{Piece, ProfileLoop, curve_nurbs, curve_polylines},
51};
52use std::collections::BTreeSet;
53
54/// A sketch as a program holds it.
55pub type Sketch = geop_core_sketch::Sketch;
56/// A sketch constraint as a program holds it.
57pub type Constraint = geop_core_sketch::Constraint;
58/// A sketch curve's kind as a program holds it.
59pub type CurveKind = geop_core_sketch::CurveKind;
60
61one_of! {
62    /// The plane a sketch is placed on: a flat face, a datum plane — the
63    /// origin's among them — or another sketch's plane.
64    pub enum SketchPlane -> CoordinateSystem {
65        Face(ElementReference<NamedModel, Plane>),
66        Datum(ElementReference<Datums, Plane>),
67        Sketch(ElementReference<PlacedSketch, Plane>),
68    }
69}
70
71/// Draws a part's sketches, each as the target it is, and lists them.
72pub struct SketchDrawer;
73
74impl Drawer for SketchDrawer {
75    fn draw(&self, reader: &Reader, view: &mut PartView) -> GeopResult<()> {
76        for target in reader.targets_of::<PlacedSketch>() {
77            let name = target.name().to_string();
78            let reference = ElementReference::<_, PlacedSketch>::whole(target);
79            reader.retrieve_element(&reference)?.draw(&name, view)?;
80        }
81        Ok(())
82    }
83
84    fn structure(&self, reader: &Reader) -> GeopResult<Vec<StructureItem>> {
85        (reader.targets_of::<PlacedSketch>().into_iter())
86            .map(|target| {
87                let reference = ElementReference::<_, PlacedSketch>::whole(target);
88                let placed = reader.retrieve_element(&reference)?;
89                Ok(StructureItem::drawn("sketch", "Sketches", &placed.name))
90            })
91            .collect()
92    }
93}
94
95/// A sketch together with the plane it lies on: `plane.u`/`plane.v` are
96/// the sketch's `x`/`y` (unit, orthogonal), `plane.w = u x v` its unit
97/// normal — and its name, the id of the step that drew it, which what is
98/// built from it is named after.
99///
100/// The plane is resolved once, when the sketch is added — a sketch drawn on
101/// a face stays where that face *was*, even after a later operation
102/// reshapes or consumes the face.
103#[derive(Clone, Debug, PartialEq)]
104pub struct PlacedSketch {
105    pub name: String,
106    pub plane: CoordinateSystem,
107    pub sketch: Sketch,
108}
109
110/// What the target of a sketch is named, before the sketch's name.
111const PREFIX: &str = "sketch:";
112
113/// The sketch `name`: what the step of that id draws.
114pub fn sketch(name: &str) -> TargetReference<PlacedSketch> {
115    TargetReference::new(format!("{PREFIX}{name}"))
116}
117
118/// A sketch of the part, read whole: what is swept.
119pub type SketchRef = ElementReference<PlacedSketch, PlacedSketch>;
120
121/// The sketch `name`, read whole: what is swept.
122pub fn whole_sketch(name: &str) -> ElementReference<PlacedSketch, PlacedSketch> {
123    ElementReference::whole(sketch(name))
124}
125
126/// The element `element` — a point `p3` or a curve `c3` — of the sketch
127/// `name`, as what `K` it is.
128pub fn sketch_element<K: Kind>(
129    name: &str,
130    element: impl ToString,
131) -> ElementReference<PlacedSketch, K>
132where
133    PlacedSketch: HasElements<K>,
134{
135    ElementReference::new(sketch(name), element.to_string())
136}
137
138/// The names of the pieces of a sketch loop or chain, and of its joints:
139/// `sketch,c3` for a piece and `sketch,p2` for a joint. An open chain also
140/// names its end joint.
141pub fn piece_names<C>(
142    sketch: &str,
143    pieces: &[Piece<C>],
144    closed: bool,
145) -> (Vec<String>, Vec<String>) {
146    let curve_names = pieces
147        .iter()
148        .map(|p| format!("{sketch},{}", p.name()))
149        .collect();
150    let mut joint_names: Vec<String> = pieces
151        .iter()
152        .map(|p| format!("{sketch},{}", p.start))
153        .collect();
154    if !closed && let Some(last) = pieces.last() {
155        joint_names.push(format!("{sketch},{}", last.end));
156    }
157    (curve_names, joint_names)
158}
159
160/// The pieces of a loop or a chain of the sketch `sketch` as a chain named
161/// after it and its elements (see [`piece_names`]).
162pub fn chain_of_pieces<const D: usize>(
163    sketch: &str,
164    pieces: Vec<Piece<NurbCurve<D>>>,
165    closed: bool,
166) -> Chain<NurbCurve<D>> {
167    let (curve_names, joint_names) = piece_names(sketch, &pieces, closed);
168    Chain {
169        name: sketch.to_string(),
170        curves: pieces.into_iter().map(|p| p.curve).collect(),
171        curve_names,
172        joint_names,
173    }
174}
175
176/// The profiles of `shape`, one of the sketch `name`'s, its points at
177/// `geometry`: each region's loops, its outer loop first and then its
178/// holes, one region after the other — or the one open chain — each named
179/// after the sketch and its elements (see [`chain_of_pieces`]).
180pub fn shape_profiles(
181    name: &str,
182    sketch: &Sketch,
183    geometry: &Enclosure,
184    shape: Shape,
185) -> GeopResult<Vec<Profile>> {
186    match shape {
187        Shape::Regions(regions) => (regions.iter())
188            .flat_map(|region| std::iter::once(&region.outer).chain(&region.holes))
189            .map(|lp| Ok(chain_of_pieces(name, lp.to_nurbs(sketch, geometry)?, true)))
190            .collect(),
191        Shape::Chain(chain) => Ok(vec![chain_of_pieces(
192            name,
193            chain.to_nurbs(sketch, geometry)?,
194            false,
195        )]),
196    }
197}
198
199impl PlacedSketch {
200    /// The curve `curve` of the sketch where its plane puts it: one NURBS
201    /// from its start to its end — a circle round from its seam — of the
202    /// sketch solved as `geometry` says (see [`Sketch::enclose`]).
203    pub fn curve_in_space(&self, curve: CurveId, geometry: &Enclosure) -> GeopResult<NurbCurve3D> {
204        let pieces = curve_nurbs(&self.sketch, geometry, curve)?;
205        let plane = &self.plane;
206        NurbCurve::join(&pieces)?.embed(plane.origin(), plane.u(), plane.v())
207    }
208
209    /// The sketch's one open chain of curves, or its one loop, in space, as
210    /// a path runs along it: its pieces and joints named as a profile's
211    /// are. Fails for a sketch of more than one loop.
212    pub fn path(&self) -> GeopResult<Chain<NurbCurve3D>> {
213        let ctx = with_context!("the curves of the sketch {:?}", self.name);
214        let sketch = &self.sketch;
215        let geometry = sketch.enclose().with_context(ctx)?;
216        let (lp, closed) = match sketch.shape().with_context(ctx)? {
217            Shape::Chain(chain) => (chain, false),
218            Shape::Regions(mut regions) if regions.len() == 1 && regions[0].holes.is_empty() => {
219                (regions.remove(0).outer, true)
220            }
221            Shape::Regions(_) => {
222                return Err(GeopError::new(
223                    "it has more than one loop: a path or a rail is one chain of curves, or one loop",
224                ))
225                .with_context(ctx);
226            }
227        };
228        let pieces = lp.to_nurbs(sketch, &geometry).with_context(ctx)?;
229        let plane = &self.plane;
230        let (curve_names, joint_names) = piece_names(&self.name, &pieces, closed);
231        Ok(Chain {
232            name: self.name.clone(),
233            curves: (pieces.iter())
234                .map(|p| p.curve.embed(plane.origin(), plane.u(), plane.v()))
235                .collect::<GeopResult<_>>()
236                .with_context(ctx)?,
237            curve_names,
238            joint_names,
239        })
240    }
241
242    /// Every run of the sketch's curves (see [`Sketch::runs`]) as a chain on
243    /// its plane, named after the sketch and its elements (see
244    /// [`chain_of_pieces`]). Where three or more curves meet, the runs share a
245    /// point but each has a name of its own for it: `K,p2` for the first run
246    /// there, `K,p2,c5` for the run starting with the curve `c5`.
247    pub fn runs(&self) -> GeopResult<Vec<Profile>> {
248        let name = &self.name;
249        let ctx = with_context!("the runs of curves of the sketch {name:?}");
250        let geometry = self.sketch.enclose().with_context(ctx)?;
251        let mut taken = BTreeSet::new();
252        let mut profiles = Vec::new();
253        for run in self.sketch.runs().with_context(ctx)? {
254            let lp = ProfileLoop { edges: run.edges };
255            let pieces = lp.to_nurbs(&self.sketch, &geometry).with_context(ctx)?;
256            let first = pieces[0].name();
257            let mut profile = chain_of_pieces(name, pieces, run.closed);
258            for joint in &mut profile.joint_names {
259                if !taken.insert(joint.clone()) {
260                    *joint = format!("{joint},{first}");
261                }
262            }
263            profiles.push(profile);
264        }
265        Ok(profiles)
266    }
267
268    /// The curve `curve` of the sketch, in space, as a chain of one curve
269    /// named after the sketch and it, its ends after the points there — a
270    /// closed one's `K,c3,seam`.
271    pub fn curve_chain(&self, curve: CurveId) -> GeopResult<Chain<NurbCurve3D>> {
272        let name = &self.name;
273        let ctx = with_context!("curve {curve} of the sketch {name:?}");
274        let geometry = self.sketch.enclose().with_context(ctx)?;
275        let class = self.sketch.point_classes();
276        let in_space = self.curve_in_space(curve, &geometry).with_context(ctx)?;
277        let joint_name = |p: PointId| match class.get(&p) {
278            Some(class) => Ok(format!("{name},{class}")),
279            None => Err(GeopError::new(format!("point {p} has no class"))),
280        };
281        let joint_names = match self.sketch.curve(curve)?.endpoints() {
282            Some((s, e)) => vec![joint_name(s)?, joint_name(e)?],
283            None => vec![format!("{name},{curve},seam")],
284        };
285        Ok(Chain {
286            name: format!("{name},{curve}"),
287            curves: vec![in_space],
288            curve_names: vec![format!("{name},{curve}")],
289            joint_names,
290        })
291    }
292}
293
294impl PlacedSketch {
295    /// Draws it into `view` as the target `target`: its regions, its curves
296    /// and its points — but not what it was only given to draw against, its
297    /// own axes and origin and what it projected, once it is done.
298    pub fn draw(&self, target: &str, view: &mut PartView) -> GeopResult<()> {
299        let s = &self.sketch;
300        // A sketch whose curves form no region is still a sketch, hit on its
301        // curves.
302        let regions = (s.regions())
303            .and_then(|regions| {
304                (regions.iter())
305                    .map(|r| {
306                        (std::iter::once(&r.outer).chain(&r.holes))
307                            .map(|l| l.polyline(s))
308                            .collect()
309                    })
310                    .collect()
311            })
312            .unwrap_or_default();
313        // A curve is drawn as its lines, a text one for each contour.
314        let mut curves = Vec::new();
315        for (&id, c) in s
316            .curves
317            .iter()
318            .filter(|(_, c)| !(c.fixed && c.construction))
319        {
320            curves.extend(
321                curve_polylines(s, id)?
322                    .into_iter()
323                    .map(|polyline| ViewCurve {
324                        id: id.to_string(),
325                        construction: c.construction,
326                        polyline,
327                    }),
328            );
329        }
330        let points = (s.positions().iter())
331            .filter(|(id, _)| s.points.get(id).is_some_and(|p| !p.fixed))
332            .map(|(id, &at)| ViewSketchPoint {
333                id: id.to_string(),
334                at,
335            })
336            .collect();
337        view.sketches.push(ViewSketch {
338            name: self.name.clone(),
339            target: target.to_string(),
340            plane: self.plane.clone(),
341            regions,
342            curves,
343            points,
344        });
345        Ok(())
346    }
347
348    /// Its point `name`, `p3`, in space.
349    fn point_in_space(&self, name: &str) -> GeopResult<Vector3> {
350        let id: PointId = name.parse()?;
351        self.sketch.point(id)?;
352        let geometry = self.sketch.enclose()?;
353        let at = (geometry.points.get(&id))
354            .ok_or_else(|| GeopError::new(format!("point {id} is not solved")))?;
355        Ok(self.plane.uv_to_xyz(at))
356    }
357
358    /// Its curve `name`, `c3`, and what kind it is.
359    fn curve_named(&self, name: &str) -> GeopResult<(CurveId, &CurveKind)> {
360        let id: CurveId = name.parse()?;
361        Ok((id, &self.sketch.curve(id)?.kind))
362    }
363
364    /// Its curve `name`, a circle or an arc of one, in space.
365    fn arc(&self, name: &str) -> GeopResult<Arc> {
366        let (id, kind) = self.curve_named(name)?;
367        if !matches!(kind, CurveKind::Arc { .. } | CurveKind::Circle { .. }) {
368            return Err(GeopError::new(format!("curve {id} is no circle or arc")));
369        }
370        let geometry = self.sketch.enclose()?;
371        (self.curve_in_space(id, &geometry)?.as_arc()?)
372            .ok_or_else(|| GeopError::new(format!("curve {id} is not circular")))
373    }
374}
375
376/// Produced once, by the step that draws it.
377impl Target for PlacedSketch {}
378
379/// Read whole: what is swept, as its regions or its curves.
380impl Kind for PlacedSketch {
381    const NAME: &'static str = drawn::SKETCH;
382}
383
384/// The elements of a sketch's target, which a pick tells from those of
385/// other targets by its name.
386macro_rules! sketch_elements {
387    ($($kind:ty => $element:ty, |$sketch:ident, $name:ident| $resolve:expr;)*) => {$(
388        impl HasElements<$kind> for PlacedSketch {
389            type Element<'a> = $element;
390
391            fn element(&self, name: &str) -> GeopResult<$element> {
392                let ($sketch, $name) = (self, name);
393                ($resolve).with_context(&|e: GeopError| {
394                    e.with_context(format!("{} {name:?} of the sketch {:?}", <$kind>::NAME, self.name))
395                })
396            }
397
398            fn names_target(name: &str) -> bool {
399                name.starts_with(PREFIX)
400            }
401        }
402    )*};
403}
404
405impl HasElements<PlacedSketch> for PlacedSketch {
406    type Element<'a> = &'a PlacedSketch;
407
408    fn element(&self, name: &str) -> GeopResult<&PlacedSketch> {
409        whole(self, name)
410    }
411
412    fn names_target(name: &str) -> bool {
413        name.starts_with(PREFIX)
414    }
415}
416
417sketch_elements! {
418    Plane => CoordinateSystem, |sketch, name| {
419        whole(sketch, name).map(|sketch| sketch.plane.clone())
420    };
421    Path => Chain<NurbCurve3D>, |sketch, name| whole(sketch, name)?.path();
422    Point => Vector3, |sketch, name| sketch.point_in_space(name);
423    Line => Axis, |sketch, name| {
424        match sketch.curve_named(name)? {
425            (_, &CurveKind::Line { start, end }) => {
426                let (a, b) = (sketch.point_in_space(&start.to_string())?, sketch.point_in_space(&end.to_string())?);
427                Axis::try_new(a, b.sub(&a))
428            }
429            (id, _) => Err(GeopError::new(format!("curve {id} is no line"))),
430        }
431    };
432    Circle => Arc, |sketch, name| sketch.arc(name);
433    Round => Axis, |sketch, name| sketch.arc(name).map(|arc| arc.circle.axis());
434    Curve => Chain<NurbCurve3D>, |sketch, name| {
435        let (id, _) = sketch.curve_named(name)?;
436        sketch.curve_chain(id)
437    };
438}
439
440/// What is picked of a sketch from what is drawn of it: the sketch as a
441/// whole, its plane or its path by a hit inside it or on it; a point by a
442/// point; a curve, as what it is, by a curve — each only where it is that.
443macro_rules! picked_from_its_drawing {
444    ($($kind:ty => $from:expr;)*) => {$(
445        impl SelectableElement<PlacedSketch> for $kind {
446            const PICKED_FROM: &'static [&'static str] = &[$from];
447
448            fn select(
449                reader: &Reader,
450                hit: &PickedElement,
451            ) -> Option<ElementReference<PlacedSketch, Self>> {
452                let is_sketch = <PlacedSketch as HasElements<$kind>>::names_target(hit.target.name());
453                (is_sketch && hit.kind == $from).then_some(())?;
454                let reference = ElementReference::new(TargetReference::located(hit.target.clone()), &hit.name);
455                reader.retrieve_element(&reference).ok()?;
456                Some(reference)
457            }
458
459            fn drawn(
460                _: &Reader,
461                element: &ElementReference<PlacedSketch, Self>,
462            ) -> Vec<PickedElement> {
463                vec![PickedElement {
464                    target: element.target().location().clone(),
465                    kind: $from.to_string(),
466                    name: element.name().to_string(),
467                }]
468            }
469        }
470    )*};
471}
472
473picked_from_its_drawing! {
474    PlacedSketch => drawn::SKETCH;
475    Plane => drawn::SKETCH;
476    Path => drawn::SKETCH;
477    Point => drawn::SKETCH_POINT;
478    Line => drawn::SKETCH_CURVE;
479    Circle => drawn::SKETCH_CURVE;
480    Round => drawn::SKETCH_CURVE;
481    Curve => drawn::SKETCH_CURVE;
482}