1use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
12
13use geop_core_math::geop_error::{GeopError, GeopResult};
14use serde::{Deserialize, Serialize};
15
16use crate::{
17 operation::{Operation, StepArgs, saved},
18 operation_registry::OperationRegistry,
19 part::Part,
20 placed::{INSTANCE_SEPARATOR, Located, TARGET_SEPARATOR},
21 runner::Runner,
22 values::StateValues,
23};
24
25#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
30pub struct ProgramStep {
31 pub id: String,
32 pub operation: String,
33 pub args: serde_json::Value,
34 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub active: Option<String>,
44}
45
46impl ProgramStep {
47 pub fn of<A: StepArgs>(id: impl Into<String>, args: &A) -> GeopResult<Self> {
49 let id = id.into();
50 let args = serde_json::to_value(args).map_err(|e| {
51 GeopError::new(format!("arguments of operation `{}`: {e}", A::KIND))
52 .with_context(format!("step `{id}`"))
53 })?;
54 Ok(Self {
55 id,
56 operation: A::KIND.to_string(),
57 args,
58 active: None,
59 })
60 }
61
62 pub fn new<O: Operation>(id: impl Into<String>, args: &O::Args) -> GeopResult<Self> {
63 let id = id.into();
64 let args = saved::<O>(args).map_err(|e| e.with_context(format!("step `{id}`")))?;
65 Ok(Self {
66 id,
67 operation: O::KIND.to_string(),
68 args,
69 active: None,
70 })
71 }
72}
73
74#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
78pub struct Program {
79 #[serde(default)]
80 pub steps: Vec<ProgramStep>,
81 #[serde(default, skip_serializing_if = "StateValues::is_empty")]
82 pub state: StateValues,
83 #[serde(default, skip_serializing_if = "Display::is_empty")]
84 pub display: Display,
85}
86
87#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
92pub struct Display {
93 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
96 pub visibility: BTreeMap<String, bool>,
97 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
100 pub colors: BTreeMap<String, String>,
101 #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
104 pub isolated: BTreeSet<String>,
105}
106
107impl Display {
108 pub fn is_empty(&self) -> bool {
109 self.visibility.is_empty() && self.colors.is_empty() && self.isolated.is_empty()
110 }
111}
112
113pub fn validate_operation_id(id: &str) -> GeopResult<()> {
119 if !id.is_empty()
120 && !id.chars().all(|c| c == '.')
121 && id
122 .chars()
123 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '.'))
124 {
125 Ok(())
126 } else {
127 Err(GeopError::new(format!(
128 "{id:?} is not a valid operation id: use only ASCII letters, digits, '_', '-' and '.'"
129 )))
130 }
131}
132
133impl Program {
134 pub fn new() -> Self {
136 Self::default()
137 }
138
139 pub fn from_steps(steps: Vec<ProgramStep>) -> Self {
141 Self {
142 steps,
143 ..Self::default()
144 }
145 }
146
147 pub fn from_json(text: &str) -> GeopResult<Self> {
152 if text.trim().is_empty() {
153 return Ok(Self::new());
154 }
155 let program: Self = serde_json::from_str(text)
156 .map_err(|e| GeopError::new(format!("reading a program: {e}")))?;
157 program.validate_ids()?;
158 Ok(program)
159 }
160
161 pub fn to_json(&self) -> GeopResult<String> {
162 serde_json::to_string_pretty(self)
163 .map_err(|e| GeopError::new(format!("writing a program: {e}")))
164 }
165
166 pub fn step(&self, id: &str) -> GeopResult<&ProgramStep> {
168 (self.steps.iter().find(|s| s.id == id)).ok_or_else(|| no_step(id))
169 }
170
171 pub fn step_mut(&mut self, id: &str) -> GeopResult<&mut ProgramStep> {
172 (self.steps.iter_mut().find(|s| s.id == id)).ok_or_else(|| no_step(id))
173 }
174
175 pub fn index_of(&self, id: &str) -> GeopResult<usize> {
177 (self.steps.iter().position(|s| s.id == id)).ok_or_else(|| no_step(id))
178 }
179
180 #[allow(
183 clippy::expect_used,
184 reason = "arguments that do not serialize are a bug of the program written"
185 )]
186 pub fn push<A: StepArgs>(&mut self, id: impl Into<String>, args: A) {
187 let step = ProgramStep::of(id, &args).expect("a step's arguments are plain data");
188 self.steps.push(step);
189 }
190
191 pub fn fresh_id(&self, label: &str) -> String {
195 let base = label.to_lowercase().replace(' ', "_");
196 (1..)
197 .map(|n| format!("{base}{n}"))
198 .find(|id| self.steps.iter().all(|s| &s.id != id))
199 .unwrap_or(base)
200 }
201
202 pub fn run(&self, runner: &Runner, part: &mut Part) -> GeopResult<()> {
206 let output = runner.run(part, self, None);
207 match output.step_errors.into_iter().next() {
208 Some((step, error)) => Err(error.with_context(format!("step {step:?}"))),
209 None => Ok(()),
210 }
211 }
212
213 pub fn excerpt(&self, id: &str) -> GeopResult<Self> {
217 let step = self.step(id)?.clone();
218 let state = (self.state.iter())
219 .filter(|(name, _)| names_step(name, id))
220 .map(|(name, value)| (name.clone(), value.clone()))
221 .collect();
222 Ok(Self {
223 steps: vec![step],
224 state,
225 display: Display::default(),
226 })
227 }
228
229 pub fn rewrite_texts(&mut self, rewrite: impl Fn(&str) -> Option<String>) {
232 fn walk(value: &mut serde_json::Value, rewrite: &dyn Fn(&str) -> Option<String>) {
233 match value {
234 serde_json::Value::String(text) => {
235 if let Some(rewritten) = rewrite(text) {
236 *text = rewritten;
237 }
238 }
239 serde_json::Value::Array(values) => {
240 values.iter_mut().for_each(|v| walk(v, rewrite));
241 }
242 serde_json::Value::Object(fields) => {
243 fields.values_mut().for_each(|v| walk(v, rewrite));
244 }
245 _ => {}
246 }
247 }
248 for step in &mut self.steps {
249 walk(&mut step.args, &rewrite);
250 }
251 }
252
253 pub fn paste(
266 &mut self,
267 pasted: Self,
268 index: usize,
269 registry: &OperationRegistry,
270 label: impl Fn(&ProgramStep) -> String,
271 ) -> GeopResult<Vec<String>> {
272 pasted
273 .validate(registry)
274 .map_err(|e| e.with_context("pasting a program"))?;
275 if index > self.steps.len() {
276 return Err(GeopError::new(format!(
277 "cannot paste at {index}: the program has {} steps",
278 self.steps.len()
279 )));
280 }
281 let own: Vec<String> = pasted.steps.iter().map(|s| s.id.clone()).collect();
282 let mut renamed = HashMap::new();
283 let mut ids = Vec::with_capacity(pasted.steps.len());
284 for (k, step) in pasted.steps.into_iter().enumerate() {
285 let id = match self.steps.iter().any(|s| s.id == step.id) {
286 true => self.fresh_id(&label(&step)),
287 false => step.id.clone(),
288 };
289 if id != step.id {
290 renamed.insert(step.id.clone(), id.clone());
291 }
292 self.steps.insert(
293 index + k,
294 ProgramStep {
295 id: id.clone(),
296 ..step
297 },
298 );
299 ids.push(id);
300 }
301 for step in self.steps.iter_mut().skip(index).take(ids.len()) {
302 rename_in_args(&mut step.args, &renamed);
303 }
304 for (name, value) in pasted.state {
305 if own.iter().any(|id| names_step(&name, id)) {
306 self.state
307 .insert(rename_steps(&name, &renamed, true), value);
308 }
309 }
310 Ok(ids)
311 }
312
313 pub fn validate_ids(&self) -> GeopResult<()> {
317 let mut ids = HashSet::new();
318 for step in &self.steps {
319 validate_operation_id(&step.id)?;
320 if !ids.insert(step.id.as_str()) {
321 return Err(GeopError::new(format!(
322 "the program has more than one step with id {:?}",
323 step.id
324 )));
325 }
326 }
327 Ok(())
328 }
329
330 pub fn validate(&self, registry: &OperationRegistry) -> GeopResult<()> {
335 self.validate_ids()?;
336 for step in &self.steps {
337 (registry.get(&step.operation))
338 .and_then(|operation| operation.check_args(&step.args))
339 .map_err(|e| e.with_context(format!("step `{}`", step.id)))?;
340 }
341 Ok(())
342 }
343}
344
345const NAME_SEPARATORS: [char; 4] = ['(', ')', ',', TARGET_SEPARATOR];
348
349fn rename_steps(name: &str, renamed: &HashMap<String, String>, key: bool) -> String {
356 map_steps(name, key, &mut |id| renamed.get(id).cloned())
357}
358
359fn names_step(key: &str, id: &str) -> bool {
362 let mut named = false;
363 map_steps(key, true, &mut |step| {
364 named |= step == id;
365 None
366 });
367 named
368}
369
370fn map_steps(name: &str, key: bool, map: &mut dyn FnMut(&str) -> Option<String>) -> String {
373 if name.starts_with(INSTANCE_SEPARATOR) {
374 let Ok(located) = Located::parse(name) else {
375 return name.to_string();
376 };
377 let path = located.path();
378 if path.is_empty() {
379 return Located::new(map_steps(located.name(), key, map)).to_string();
380 }
381 let (instance, rest) = path.split_once(INSTANCE_SEPARATOR).unwrap_or((path, ""));
382 let Some(to) = map(instance) else {
383 return name.to_string();
384 };
385 let path = match rest.is_empty() {
386 true => to,
387 false => format!("{to}{INSTANCE_SEPARATOR}{rest}"),
388 };
389 return Located::in_instance(path, located.name()).to_string();
390 }
391 let mut spelled = String::with_capacity(name.len());
392 let mut rest = name;
393 let mut first = true;
394 loop {
395 let end = rest.find(NAME_SEPARATORS).unwrap_or(rest.len());
396 let (piece, after) = rest.split_at(end);
397 let mapped = match map(piece) {
400 Some(to) => Some(to),
401 None if key && first => (piece.match_indices('.'))
402 .find_map(|(at, _)| map(&piece[..at]).map(|to| format!("{to}{}", &piece[at..]))),
403 None => None,
404 };
405 spelled.push_str(mapped.as_deref().unwrap_or(piece));
406 let Some(separator) = after.chars().next() else {
407 return spelled;
408 };
409 spelled.push(separator);
410 rest = &after[separator.len_utf8()..];
411 first = false;
412 }
413}
414
415fn rename_in_args(args: &mut serde_json::Value, renamed: &HashMap<String, String>) {
420 if renamed.is_empty() {
421 return;
422 }
423 match args {
424 serde_json::Value::String(text) => *text = rename_steps(text, renamed, false),
425 serde_json::Value::Array(values) => {
426 values.iter_mut().for_each(|v| rename_in_args(v, renamed));
427 }
428 serde_json::Value::Object(fields) => {
429 let placed = (fields.get("target").and_then(|t| t.as_str()))
430 .and_then(|t| Located::parse(t).ok())
431 .is_some_and(|t| !t.path().is_empty());
432 for (key, value) in fields.iter_mut() {
433 if !(placed && key == "name") {
434 rename_in_args(value, renamed);
435 }
436 }
437 }
438 _ => {}
439 }
440}
441
442fn no_step(id: &str) -> GeopError {
443 GeopError::new(format!("the program has no step `{id}`"))
444}
445
446#[cfg(test)]
447mod tests {
448 use super::*;
449
450 #[test]
455 fn a_step_renamed_is_renamed_in_every_name_of_it() {
456 let renamed = HashMap::from([("hand".to_string(), "part1".to_string())]);
457 let name = |text: &str| rename_steps(text, &renamed, false);
458 assert_eq!(name("/:sketch:hand"), "/:sketch:part1");
459 assert_eq!(name("/hand:model"), "/part1:model");
460 assert_eq!(name("/hand/hand:model"), "/part1/hand:model");
461 assert_eq!(name("/fore:hand"), "/fore:hand");
462 assert_eq!(name("extrude(hand,end)"), "extrude(part1,end)");
463 assert_eq!(name("hand.geop"), "hand.geop");
464 assert_eq!(name("handle"), "handle");
465 let key = |text: &str| rename_steps(text, &renamed, true);
466 assert_eq!(key("hand.pose"), "part1.pose");
467 assert_eq!(key("add_part(hand,m1).turn"), "add_part(part1,m1).turn");
468 assert_eq!(key("/hand:b.pose"), "/part1:b.pose");
469 assert!(names_step("add_part(hand,m1).turn", "hand"));
470 assert!(!names_step("add_part(fore,m1).turn", "hand"));
471 let mut args = serde_json::json!({
472 "entities": [{"target": "/hand:model", "name": "block(hand,c1)"},
473 {"target": "/:model", "name": "block(hand,c1)"}],
474 });
475 rename_in_args(&mut args, &renamed);
476 let expected = serde_json::json!({
477 "entities": [{"target": "/part1:model", "name": "block(hand,c1)"},
478 {"target": "/:model", "name": "block(part1,c1)"}],
479 });
480 assert_eq!(args, expected);
481 }
482}