1use 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#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
57pub struct AddSketch3d;
58
59#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
61#[serde(tag = "target", rename_all = "snake_case")]
62pub enum Target {
63 Point { point: PointId },
66 Curve { curve: CurveId },
68 Direction { constraint: ConstraintId },
72}
73
74#[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 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 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 pub sketch: Sketch3d,
118 #[serde(default, skip_serializing_if = "Vec::is_empty")]
120 pub references: Vec<Reference3d>,
121}
122
123impl AddSketch3dArgs {
124 pub fn reference_of(&self, target: Target) -> Option<&Reference3d> {
126 self.references.iter().find(|r| r.target() == target)
127 }
128
129 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 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 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 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
228fn 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
244fn 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
256fn 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 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
314pub 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 fn new_args(_: &Context) -> AddSketch3dArgs {
335 AddSketch3dArgs::default()
336 }
337
338 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 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 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}