Skip to main content

geop_ops_sketch3d/
add_sketch3d.rs

1//! [`AddSketch3d`]: draw a sketch in space.
2
3use geop_core_editor::{
4    edit_form::{Edit, EditForm, FormState},
5    editable::{Context, OperationGroup, OperationInfo, OperationTier},
6    editing::CanvasEvent,
7    part_operation,
8    part_operation::PartOperation,
9};
10use geop_core_program::{
11    element::{ElementReference, PickedElement},
12    part::StepContext,
13    reader::Reader,
14};
15use std::collections::{BTreeMap, BTreeSet};
16
17use geop_core_math::{
18    geop_error::{GeopError, GeopResult, WithContext},
19    vector::Vector3,
20    with_context,
21};
22use geop_core_sketch::{
23    ConstraintId, CurveId, PointId,
24    space::{Constraint3d, CurveKind3d, WireEdge},
25};
26use geop_core_topology::build::{BodySpec, EdgeSpec};
27use geop_ops_model::{
28    BodyNames, ModelOperation, NamedModel, Namer, build_model,
29    sketch3d::{CurveSource, LineSource, PlacedSketch3d, PointSource, Sketch3d, sketch3d},
30};
31use serde::{Deserialize, Serialize};
32
33use crate::editor::{self, Sketch3dSession};
34
35/// Adds a sketch in space to the part, named by the operation's id: points,
36/// lines, arcs through three points and splines through points, with the
37/// constraints of a 3-D sketch (see [`geop_core_sketch::space`]). The step
38/// `K` defines the part's 3-D sketch `K` (see [`sketch3d`]),
39/// a path as a whole, and builds it as a wire: an edge for every curve
40/// drawn, construction curves aside, and a vertex for every point a curve
41/// of it ends at, every point two curves pass through, and every point that
42/// stands on its own. Points made one by a coincidence are one vertex,
43/// named after the lowest of their ids: the edge of curve `c3` is named
44/// `sketch3d(K,c3)`, the vertex of point `p1` `sketch3d(K,p1)`. A curve
45/// passing through a point another curve passes through too — a spline
46/// drawn through it, an arc's middle point — is cut there, into edges named
47/// after the curve and the point each starts at: `sketch3d(K,c3,p0)`,
48/// `sketch3d(K,c3,p1)`. Curves that meet anywhere else, or two points at
49/// one place that are not one, are refused by name (see
50/// [`geop_core_sketch::space::Sketch3d::check_wire`]).
51///
52/// What it is given from the part — a point at a vertex or a datum point, a
53/// point on an edge, a line parallel to an axis or a straight edge — is
54/// brought up to date every time it is built (see [`Reference3d`]), so the
55/// sketch follows the part.
56#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
57pub struct AddSketch3d;
58
59/// What a [`Reference3d`] gives the sketch.
60#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
61#[serde(tag = "target", rename_all = "snake_case")]
62pub enum Target {
63    /// Where the fixed point `point` is: the entity's point — a vertex, a
64    /// datum point, a frame's origin, a sketch's point.
65    Point { point: PointId },
66    /// The geometry of the reference curve `curve`: the entity's — an edge.
67    Curve { curve: CurveId },
68    /// The direction of the constraint `constraint` — a
69    /// [`Constraint3d::ParallelTo`] or [`Constraint3d::TangentTo`]: the
70    /// entity's line — an axis, a straight edge, a sketch's line.
71    Direction { constraint: ConstraintId },
72}
73
74/// Something of the sketch given by an element of the part: where a fixed
75/// point is, what a reference curve is, which way a direction runs.
76#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
77#[serde(tag = "target", rename_all = "snake_case")]
78pub enum Reference3d {
79    Point {
80        point: PointId,
81        from: PointSource,
82    },
83    Curve {
84        curve: CurveId,
85        from: CurveSource,
86    },
87    Direction {
88        constraint: ConstraintId,
89        from: LineSource,
90    },
91}
92
93impl Reference3d {
94    /// What of the sketch it gives.
95    pub fn target(&self) -> Target {
96        match *self {
97            Reference3d::Point { point, .. } => Target::Point { point },
98            Reference3d::Curve { curve, .. } => Target::Curve { curve },
99            Reference3d::Direction { constraint, .. } => Target::Direction { constraint },
100        }
101    }
102
103    /// What of the part gives it, as a pick names it.
104    pub fn source(&self) -> PickedElement {
105        use geop_core_editor::selectable::Selectable;
106        match self {
107            Reference3d::Point { from, .. } => from.picked(),
108            Reference3d::Curve { from, .. } => from.picked(),
109            Reference3d::Direction { from, .. } => from.picked(),
110        }
111    }
112}
113
114#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
115pub struct AddSketch3dArgs {
116    /// The sketch as drawn.
117    pub sketch: Sketch3d,
118    /// What of it the part gives.
119    #[serde(default, skip_serializing_if = "Vec::is_empty")]
120    pub references: Vec<Reference3d>,
121}
122
123impl AddSketch3dArgs {
124    /// The reference that gives `target`, if any.
125    pub fn reference_of(&self, target: Target) -> Option<&Reference3d> {
126        self.references.iter().find(|r| r.target() == target)
127    }
128
129    /// Forgets the references whose target the sketch no longer has.
130    fn forget_dead_references(&mut self) {
131        let sketch = &self.sketch;
132        self.references.retain(|r| match r.target() {
133            Target::Point { point } => sketch.points.contains_key(&point),
134            Target::Curve { curve } => sketch.curves.contains_key(&curve),
135            Target::Direction { constraint } => sketch.constraints.contains_key(&constraint),
136        });
137    }
138
139    /// Brings what the part gives the sketch up to date with the part
140    /// `reader` reads: moves its fixed points, hands in its reference
141    /// curves' geometry, turns its directions. Forgets references whose
142    /// target is gone.
143    pub fn resolve(&mut self, reader: &Reader) -> GeopResult<()> {
144        self.forget_dead_references();
145        for r in &self.references {
146            let ctx = with_context!("the 3-D sketch's reference to {}", r.source().label());
147            match r {
148                Reference3d::Point { point, from } => {
149                    let at = from.resolve(reader).with_context(ctx)?;
150                    let p = (self.sketch.points.get_mut(point)).ok_or_else(|| {
151                        GeopError::new(format!("the sketch has no point {point}"))
152                    })?;
153                    p.at = at;
154                    p.fixed = true;
155                }
156                Reference3d::Curve { curve, from } => {
157                    let chain = from.resolve(reader).with_context(ctx)?;
158                    let [edge] = chain.curves.as_slice() else {
159                        return Err(GeopError::new("it is more than one curve")).with_context(ctx);
160                    };
161                    self.sketch.references.insert(*curve, edge.clone());
162                }
163                &Reference3d::Direction {
164                    constraint,
165                    ref from,
166                } => {
167                    let d = from.resolve(reader).with_context(ctx)?.direction;
168                    match self.sketch.constraints.get_mut(&constraint) {
169                        Some(
170                            Constraint3d::ParallelTo { direction, .. }
171                            | Constraint3d::TangentTo { direction, .. },
172                        ) => *direction = d,
173                        other => {
174                            return Err(GeopError::new(format!(
175                                "a direction is given to constraint {constraint} = {other:?}, which has none"
176                            )))
177                            .with_context(ctx);
178                        }
179                    }
180                }
181            }
182        }
183        Ok(())
184    }
185
186    /// Forgets the reference curves no constraint uses any more, and the
187    /// references to what is gone.
188    pub fn tidy(&mut self) {
189        let unused: Vec<CurveId> = self
190            .sketch
191            .curves
192            .iter()
193            .filter(|(id, c)| {
194                c.kind == CurveKind3d::Reference
195                    && !self
196                        .sketch
197                        .constraints
198                        .values()
199                        .any(|k| k.curves().contains(id))
200            })
201            .map(|(&id, _)| id)
202            .collect();
203        self.sketch.remove(&[], &unused, &[]);
204        self.forget_dead_references();
205    }
206
207    /// The sketch built on the part `reader` reads: what the part gives it
208    /// brought up to date, solved anew where its constraints no longer hold
209    /// as drawn — a sketch as it was drawn is as it was solved — and every
210    /// curve of it checked to build.
211    pub fn build(&self, reader: &Reader) -> GeopResult<Sketch3d> {
212        let mut args = self.clone();
213        args.resolve(reader)?;
214        let mut sketch = args.sketch;
215        if !sketch.check()?.converged {
216            sketch.solve()?;
217        }
218        let geometry = sketch.enclose()?;
219        for (&id, curve) in &sketch.curves {
220            if curve.is_drawn() {
221                sketch.curve_nurbs(id, &geometry)?;
222            }
223        }
224        Ok(sketch)
225    }
226}
227
228/// The 3-D sketch the step `step` draws with `args`, on the part before it,
229/// and the names of the edges it is built as.
230fn draw(reader: &Reader, (step, args): &(String, AddSketch3dArgs)) -> GeopResult<PlacedSketch3d> {
231    let ctx = with_context!("add_sketch3d({step})");
232    let sketch = args.build(reader).with_context(ctx)?;
233    let namer = Namer::new("sketch3d", step).with_context(ctx)?;
234    let edges = (sketch.wire_edges().with_context(ctx)?.iter())
235        .map(|edge| edge_name(&namer, edge))
236        .collect();
237    Ok(PlacedSketch3d {
238        name: step.clone(),
239        sketch,
240        edges,
241    })
242}
243
244/// What the wire's edge `edge` is named (see [`AddSketch3d`]): after its
245/// curve, and, for a piece of a curve cut at the points it shares, after
246/// the point it starts at too.
247fn edge_name(namer: &Namer, edge: &WireEdge) -> String {
248    let curve = edge.curve.to_string();
249    if edge.cut {
250        namer.piece(&curve, &edge.start.to_string())
251    } else {
252        namer.name(&[&curve])
253    }
254}
255
256/// The wire `sketch` is built as, and the names of its vertices and edges
257/// (see [`AddSketch3d`]): `None` for a sketch with no point to build.
258fn wire_of(sketch: &Sketch3d, namer: &Namer) -> GeopResult<Option<(BodySpec, BodyNames)>> {
259    let class = sketch.point_classes();
260    let geometry = sketch.enclose()?;
261    sketch.check_wire(&geometry)?;
262    let edges = sketch.wire_edges()?;
263    let used = |p: &PointId| {
264        (sketch.curves.values()).any(|c| c.points().iter().any(|q| class[q] == class[p]))
265    };
266    let ends = edges.iter().flat_map(|edge| [edge.start, edge.end]);
267    let alone = sketch.points.keys().copied().filter(|p| !used(p));
268    let points: BTreeSet<PointId> = ends.chain(alone).map(|p| class[&p]).collect();
269    if points.is_empty() {
270        return Ok(None);
271    }
272    // A vertex encloses every point of its class.
273    let enclosures = sketch.point_enclosures(&geometry);
274    let vertices: BTreeMap<PointId, Vector3> =
275        points.into_iter().map(|p| (p, enclosures[&p])).collect();
276    let index: BTreeMap<PointId, usize> =
277        vertices.keys().enumerate().map(|(i, &p)| (p, i)).collect();
278    let mut curves = Vec::new();
279    for (&id, _) in sketch.drawn_curves() {
280        curves.extend(sketch.wire_edge_nurbs(id, &geometry)?);
281    }
282    if curves.len() != edges.len() {
283        return Err(GeopError::new(format!(
284            "the wire has {} edges but {} curves",
285            edges.len(),
286            curves.len()
287        )));
288    }
289    let specs = (edges.iter().zip(curves))
290        .map(|(edge, curve)| EdgeSpec {
291            curve,
292            start: index[&edge.start],
293            end: index[&edge.end],
294        })
295        .collect();
296    let names = BodyNames {
297        vertices: (vertices.keys())
298            .map(|p| namer.name(&[&p.to_string()]))
299            .collect(),
300        edges: edges.iter().map(|edge| edge_name(namer, edge)).collect(),
301        faces: Vec::new(),
302        solid: None,
303    };
304    let spec = BodySpec {
305        vertices: vertices.into_values().collect(),
306        edges: specs,
307        faces: Vec::new(),
308        shells: Vec::new(),
309        solid: false,
310    };
311    Ok(Some((spec, names)))
312}
313
314/// What a 3-D sketch step keeps while it is edited: the drawing's session,
315/// and the keys of the visuals selected.
316pub type Sketch3dEditing = FormState<Sketch3dSession>;
317
318part_operation!(AddSketch3d(AddSketch3dArgs));
319
320impl PartOperation for AddSketch3d {
321    const KIND: &'static str = "add_sketch3d";
322    const INFO: OperationInfo = OperationInfo::new(
323        "3-D sketch",
324        "Draw points, lines, arcs and splines in space: paths to sweep along.",
325        OperationGroup::Sketch,
326        OperationTier::Small,
327    )
328    .shortcut("Shift+S")
329    .icon(crate::icons::ADD_SKETCH3D);
330    type Args = AddSketch3dArgs;
331    type Session = Sketch3dSession;
332
333    /// An empty sketch: the first click starts drawing.
334    fn new_args(_: &Context) -> AddSketch3dArgs {
335        AddSketch3dArgs::default()
336    }
337
338    /// Defines the 3-D sketch named after the step, and builds it as a
339    /// wire.
340    fn run(ctx: &mut StepContext, args: &AddSketch3dArgs) -> GeopResult<()> {
341        let step = ctx.step();
342        ctx.produce(&sketch3d(step), &(step.to_string(), args.clone()), draw)?;
343        build_model::<Self>(ctx, args)
344    }
345
346    /// The tools, the coordinates of a point, the constraints: see
347    /// [`crate::editor`].
348    fn form<'a>(
349        context: &'a Context<'a>,
350        args: &AddSketch3dArgs,
351        editing: &Sketch3dEditing,
352    ) -> EditForm<'a, AddSketch3dArgs, Sketch3dSession> {
353        editor::form(&context.before, args, &editing.session, &editing.selection)
354    }
355
356    fn canvas(
357        context: &Context,
358        edit: Edit<'_, AddSketch3dArgs, Sketch3dSession>,
359        event: &CanvasEvent,
360    ) {
361        editor::event(&context.before, context.scene, edit, event)
362    }
363}
364
365impl ModelOperation for AddSketch3d {
366    /// Builds the 3-D sketch the step defined as a wire (see
367    /// [`AddSketch3d`]).
368    fn build(
369        model: &mut NamedModel,
370        reader: &Reader,
371        step: &str,
372        _: &AddSketch3dArgs,
373    ) -> GeopResult<()> {
374        let whole = ElementReference::<PlacedSketch3d, PlacedSketch3d>::whole(sketch3d(step));
375        let placed = reader.retrieve_element(&whole)?;
376        let namer = Namer::new("sketch3d", step)?;
377        if let Some((spec, names)) = wire_of(&placed.sketch, &namer)? {
378            model.build_body(spec, names)?;
379        }
380        Ok(())
381    }
382}