1use std::collections::HashSet;
10
11use geop_core_math::{
12 geop_error::{GeopError, GeopResult, WithContext},
13 scalars::Scalar,
14 with_context,
15};
16use geop_core_part::{Part, validate_operation_id};
17use serde::{Deserialize, Serialize};
18
19use crate::operation::{Handle, PartOperation};
20
21#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
25pub struct Step {
26 pub id: String,
27 #[serde(flatten)]
28 pub operation: PartOperation,
29}
30
31#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
37pub struct Program {
38 pub steps: Vec<Step>,
39}
40
41#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
48#[serde(tag = "edit", rename_all = "snake_case")]
49pub enum ProgramEdit {
50 Insert {
54 index: usize,
55 #[serde(default)]
56 id: Option<String>,
57 #[serde(flatten)]
58 operation: PartOperation,
59 },
60 Update {
62 id: String,
63 #[serde(flatten)]
64 operation: PartOperation,
65 },
66 Remove { id: String },
69 Move { id: String, index: usize },
71 Replace { program: Program },
73}
74
75impl ProgramEdit {
76 pub fn summary(&self) -> String {
79 match self {
80 ProgramEdit::Insert {
81 index, operation, ..
82 } => format!("insert a {} step at {index}", operation.kind()),
83 ProgramEdit::Update { id, operation } => {
84 format!("update step {id:?} to a {} step", operation.kind())
85 }
86 ProgramEdit::Remove { id } => format!("remove step {id:?}"),
87 ProgramEdit::Move { id, index } => format!("move step {id:?} to {index}"),
88 ProgramEdit::Replace { program } => {
89 format!(
90 "replace the program by one of {} steps",
91 program.steps.len()
92 )
93 }
94 }
95 }
96}
97
98impl Program {
99 pub fn new() -> Self {
100 Self::default()
101 }
102
103 pub fn push(&mut self, id: impl Into<String>, operation: impl Into<PartOperation>) {
105 self.steps.push(Step {
106 id: id.into(),
107 operation: operation.into(),
108 });
109 }
110
111 pub fn index_of(&self, id: &str) -> GeopResult<usize> {
113 self.steps
114 .iter()
115 .position(|s| s.id == id)
116 .ok_or_else(|| GeopError::new(format!("program has no step {id:?}")))
117 }
118
119 pub fn fresh_id(&self, operation: &PartOperation) -> String {
123 let base = operation.label().to_lowercase().replace(' ', "_");
124 (1..)
125 .map(|n| format!("{base}{n}"))
126 .find(|id| self.steps.iter().all(|s| &s.id != id))
127 .expect("some number is free")
128 }
129
130 pub fn validate(&self) -> GeopResult<()> {
133 let mut ids = HashSet::new();
134 for step in &self.steps {
135 validate_operation_id(&step.id)?;
136 if !ids.insert(step.id.as_str()) {
137 return Err(GeopError::new(format!(
138 "program has more than one step with id {:?}",
139 step.id
140 )));
141 }
142 }
143 Ok(())
144 }
145
146 pub fn update(&mut self, edit: ProgramEdit) -> GeopResult<Option<String>> {
154 let summary = edit.summary();
155 let ctx = with_context!("Program::update({summary})");
156 let mut next = self.clone();
157 let changed = match edit {
158 ProgramEdit::Insert {
159 index,
160 id,
161 operation,
162 } => {
163 if index > next.steps.len() {
164 return Err(GeopError::new(format!(
165 "cannot insert at {index}: the program has {} steps",
166 next.steps.len()
167 )))
168 .with_context(ctx);
169 }
170 let id = id.unwrap_or_else(|| next.fresh_id(&operation));
171 next.steps.insert(
172 index,
173 Step {
174 id: id.clone(),
175 operation,
176 },
177 );
178 Some(id)
179 }
180 ProgramEdit::Update { id, operation } => {
181 let index = next.index_of(&id).with_context(ctx)?;
182 next.steps[index].operation = operation;
183 Some(id)
184 }
185 ProgramEdit::Remove { id } => {
186 let index = next.index_of(&id).with_context(ctx)?;
187 next.steps.remove(index);
188 None
189 }
190 ProgramEdit::Move { id, index } => {
191 let from = next.index_of(&id).with_context(ctx)?;
192 let step = next.steps.remove(from);
193 if index > next.steps.len() {
194 return Err(GeopError::new(format!(
195 "cannot move to {index}: the program has {} other steps",
196 next.steps.len()
197 )))
198 .with_context(ctx);
199 }
200 next.steps.insert(index, step);
201 Some(id)
202 }
203 ProgramEdit::Replace { program } => {
204 next = program;
205 None
206 }
207 };
208 next.validate().with_context(ctx)?;
209 *self = next;
210 Ok(changed)
211 }
212
213 pub fn apply<S: Scalar>(&self, part: Part<S>) -> GeopResult<Part<S>> {
222 self.validate()?;
223 let mut part = part;
224 for (index, step) in self.steps.iter().enumerate() {
225 part = run_step(part, index, step)?;
226 }
227 Ok(part)
228 }
229
230 pub fn to_json(&self) -> GeopResult<String> {
233 serde_json::to_string_pretty(self)
234 .map_err(|e| GeopError::new(format!("serializing program: {e}")))
235 }
236
237 pub fn from_json(json: &str) -> GeopResult<Self> {
238 let program: Self = serde_json::from_str(json)
239 .map_err(|e| GeopError::new(format!("reading program: {e}")))?;
240 program.validate()?;
241 Ok(program)
242 }
243}
244
245fn run_step<S: Scalar>(part: Part<S>, index: usize, step: &Step) -> GeopResult<Part<S>> {
247 let ctx = with_context!("program step {index} ({:?})", step.id);
248 let part = step.operation.apply(part, &step.id).with_context(ctx)?;
249 part.check_names().with_context(ctx)?;
250 Ok(part)
251}
252
253#[derive(Clone, Debug, PartialEq, Serialize)]
255pub struct StepHandle {
256 pub step: String,
257 #[serde(flatten)]
258 pub handle: Handle,
259}
260
261#[derive(Clone, Debug, PartialEq, Serialize)]
263pub struct StepResult {
264 pub id: String,
265 pub error: Option<String>,
267}
268
269pub struct ProgramRunner<S: Scalar> {
283 steps: Vec<Step>,
285 parts: Vec<Part<S>>,
288 results: Vec<StepResult>,
289 ran: usize,
291}
292
293impl<S: Scalar> ProgramRunner<S> {
294 pub fn new() -> Self {
295 Self {
296 steps: Vec::new(),
297 parts: vec![Part::new()],
298 results: Vec::new(),
299 ran: 0,
300 }
301 }
302
303 pub fn run(&mut self, program: &Program, stop: Option<usize>) {
308 let common = self
309 .steps
310 .iter()
311 .zip(&program.steps)
312 .take_while(|(a, b)| a == b)
313 .count();
314 self.steps.truncate(common);
315 self.parts.truncate(common + 1);
316 self.results.truncate(common);
317
318 let target = stop.unwrap_or(program.steps.len()).min(program.steps.len());
319 let failed = |results: &[StepResult]| results.iter().any(|r| r.error.is_some());
320 while self.steps.len() < target && !failed(&self.results) {
321 let index = self.steps.len();
322 let step = &program.steps[index];
323 let before = self.parts.last().expect("parts is never empty");
324 let (part, error) = match run_step(before.clone(), index, step) {
325 Ok(part) => (part, None),
326 Err(e) => (before.clone(), Some(e.to_string())),
327 };
328 self.steps.push(step.clone());
329 self.parts.push(part);
330 self.results.push(StepResult {
331 id: step.id.clone(),
332 error,
333 });
334 }
335 let first_failure = self.results.iter().position(|r| r.error.is_some());
337 self.ran = match first_failure {
338 Some(f) if f < target => f + 1,
339 _ => target.min(self.steps.len()),
340 };
341 }
342
343 pub fn part(&self) -> &Part<S> {
345 &self.parts[self.ran]
346 }
347
348 pub fn results(&self) -> &[StepResult] {
350 &self.results[..self.ran]
351 }
352
353 pub fn handles(&self) -> GeopResult<Vec<StepHandle>> {
357 let mut handles = Vec::new();
358 for (i, (step, result)) in self.steps.iter().zip(self.results()).enumerate() {
359 if result.error.is_some() {
360 continue;
361 }
362 let ctx = with_context!("handles of step {i} ({:?})", step.id);
363 for handle in step.operation.handles(&self.parts[i]).with_context(ctx)? {
364 handles.push(StepHandle {
365 step: step.id.clone(),
366 handle,
367 });
368 }
369 }
370 Ok(handles)
371 }
372}
373
374impl<S: Scalar> Default for ProgramRunner<S> {
375 fn default() -> Self {
376 Self::new()
377 }
378}
379
380#[cfg(test)]
381mod tests {
382 use geop_core_math::scalars::ScalInF64 as S;
383 use geop_core_part::PartDescription;
384
385 use super::*;
386 use crate::{ExtrudeArgs, examples::box_with_drill_hole};
387
388 fn extrude(sketch: &str, distance: f64) -> PartOperation {
389 ExtrudeArgs {
390 sketch: sketch.into(),
391 distance,
392 symmetric: false,
393 combine: crate::Combine::NewBody,
394 }
395 .into()
396 }
397
398 #[test]
401 fn edits_change_the_program_by_id() {
402 let mut program = box_with_drill_hole();
403 let len = program.steps.len();
404
405 let id = program
406 .update(ProgramEdit::Insert {
407 index: len,
408 id: None,
409 operation: extrude("outline", 2.0),
410 })
411 .unwrap();
412 assert_eq!(id.as_deref(), Some("extrude1"));
413 assert_eq!(program.steps[len].id, "extrude1");
414
415 program
416 .update(ProgramEdit::Update {
417 id: "extrude1".into(),
418 operation: extrude("outline", 3.0),
419 })
420 .unwrap();
421 assert_eq!(program.steps[len].operation, extrude("outline", 3.0));
422
423 program
424 .update(ProgramEdit::Move {
425 id: "extrude1".into(),
426 index: 0,
427 })
428 .unwrap();
429 assert_eq!(program.steps[0].id, "extrude1");
430
431 program
432 .update(ProgramEdit::Remove {
433 id: "extrude1".into(),
434 })
435 .unwrap();
436 assert_eq!(program, box_with_drill_hole());
437
438 let before = program.clone();
439 for bad in [
440 ProgramEdit::Insert {
441 index: len + 1,
442 id: None,
443 operation: extrude("outline", 1.0),
444 },
445 ProgramEdit::Insert {
446 index: 0,
447 id: Some("box".into()),
448 operation: extrude("outline", 1.0),
449 },
450 ProgramEdit::Insert {
451 index: 0,
452 id: Some("not an id".into()),
453 operation: extrude("outline", 1.0),
454 },
455 ProgramEdit::Remove { id: "nope".into() },
456 ProgramEdit::Move {
457 id: "box".into(),
458 index: len,
459 },
460 ] {
461 assert!(program.update(bad.clone()).is_err(), "{bad:?}");
462 assert_eq!(program, before, "a rejected {bad:?} changed the program");
463 }
464 }
465
466 #[test]
468 fn edits_read_from_json() {
469 let edit: ProgramEdit = serde_json::from_str(
470 r#"{"edit": "insert", "index": 0, "operation": "extrude",
471 "args": {"sketch": "outline", "distance": 2.0}}"#,
472 )
473 .unwrap();
474 assert_eq!(
475 edit,
476 ProgramEdit::Insert {
477 index: 0,
478 id: None,
479 operation: extrude("outline", 2.0),
480 }
481 );
482 }
483
484 #[test]
488 fn runner_stops_early_and_reuses_the_unchanged_prefix() {
489 let program = box_with_drill_hole();
490 let describe = |part: &Part<S>| PartDescription::of(part).unwrap();
491 let mut runner = ProgramRunner::<S>::new();
492
493 runner.run(&program, None);
494 assert!(runner.results().iter().all(|r| r.error.is_none()));
495 assert_eq!(
496 describe(runner.part()),
497 describe(&program.apply(Part::new()).unwrap())
498 );
499
500 runner.run(&program, Some(2));
502 assert_eq!(runner.results().len(), 2);
503 let description = describe(runner.part());
504 assert_eq!(
505 description.solids.keys().collect::<Vec<_>>(),
506 ["extrude(box)"]
507 );
508
509 let mut edited = program.clone();
512 edited
513 .update(ProgramEdit::Update {
514 id: "hole".into(),
515 operation: extrude("hole_sketch", -0.25),
516 })
517 .unwrap();
518 runner.run(&edited, None);
519 assert!(runner.results().iter().all(|r| r.error.is_none()));
520 assert_eq!(
521 describe(runner.part()),
522 describe(&edited.apply(Part::new()).unwrap())
523 );
524 }
525
526 #[test]
530 fn runner_provides_every_handle() {
531 use crate::operation::{HandleGroup, HandleMotion};
532 let mut runner = ProgramRunner::<S>::new();
533 runner.run(&box_with_drill_hole(), None);
534 let handles = runner.handles().unwrap();
535 let feature: Vec<&StepHandle> = handles
536 .iter()
537 .filter(|h| h.handle.group == HandleGroup::Feature)
538 .collect();
539 assert_eq!(feature.len(), 2);
540 let close = |a: [f64; 3], b: [f64; 3]| (0..3).all(|k| (a[k] - b[k]).abs() < 1e-9);
541 let (boxed, hole) = (feature[0], feature[1]);
542 assert_eq!(boxed.step, "box");
543 assert!(close(boxed.handle.position, [1.0, 1.0, 1.0]), "{boxed:?}");
544 assert_eq!(hole.step, "hole");
545 assert!(close(hole.handle.position, [1.0, 1.0, 0.5]), "{hole:?}");
546 let HandleMotion::Linear {
547 direction,
548 arg,
549 value,
550 scale,
551 } = &hole.handle.motion
552 else {
553 panic!("{hole:?}")
554 };
555 assert!(close(*direction, [0.0, 0.0, 1.0]));
556 assert_eq!(arg, &["distance"]);
557 assert_eq!((*value, *scale), (-0.5, 1.0));
558 let sketch = handles.len() - feature.len();
560 assert_eq!(sketch, 5);
561 let json = serde_json::to_value(&handles[0]).unwrap();
562 assert_eq!(json["step"], "outline");
563 assert_eq!(json["motion"], "planar");
564 }
565
566 #[test]
569 fn runner_reports_the_failing_step() {
570 let mut program = box_with_drill_hole();
571 program
572 .update(ProgramEdit::Update {
573 id: "hole".into(),
574 operation: ExtrudeArgs {
575 sketch: "hole_sketch".into(),
576 distance: -0.5,
577 symmetric: false,
578 combine: crate::Combine::Difference {
579 target: "extrude(nothing)".into(),
580 },
581 }
582 .into(),
583 })
584 .unwrap();
585 let mut runner = ProgramRunner::<S>::new();
586 runner.run(&program, None);
587 let last = runner.results().last().unwrap();
588 assert_eq!(last.id, "hole");
589 assert!(last.error.as_deref().unwrap().contains("extrude(nothing)"));
590 assert!(runner.part().solid_id("extrude(box)").is_ok());
591 }
592}