1use std::collections::HashMap;
2use std::f64::consts::PI;
3
4use glam::Vec3;
5
6use crate::{
7 ColourMode, CurveInterpolation, CurveInterpolationKind, Diagnostic, DiagnosticKind,
8 PlotMetadata, PlotSpec, PlotStyle, PointAnnotation, SliceAxis, TableDataSet,
9 default_slice_position, eval_curve_point, eval_with_vars, parse_curve_expr,
10 parse_expr_with_vars, sample_curve_points,
11};
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub enum AnalysisKind {
15 InterpolateCurve,
16 DifferentiateCurve,
17 AxisDerivativeCurve,
18 IntegralCurve,
19 ArcLengthCurve,
20 CurvatureCurve,
21 TangentField,
22 NormalField,
23 BinormalField,
24 ExtractPoints,
25 ScalarSlice,
26 VectorSlice,
27 GradientField,
28 DivergenceField,
29 CurlField,
30 SurfaceIntersection,
31}
32
33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
34pub enum AnalysisOutputKind {
35 PlotSpec,
36 NumericReport,
37 Table,
38 Composite,
39}
40
41#[derive(Clone, Copy, Debug, PartialEq, Eq)]
42pub enum AnalysisTargetKind {
43 Definition,
44 SampledData,
45 Geometry,
46 PlotPair,
47}
48
49#[derive(Clone, Debug, PartialEq)]
50pub struct AnalysisCapability {
51 pub kind: AnalysisKind,
52 pub target_kind: AnalysisTargetKind,
53 pub output_kind: AnalysisOutputKind,
54 pub parameters: Vec<&'static str>,
55}
56
57#[derive(Clone, Debug, PartialEq)]
58pub enum AnalysisTarget {
59 Plot { index: usize, name: Option<String> },
60 PlotPair { first: usize, second: usize },
61}
62
63#[derive(Clone, Debug, PartialEq)]
64pub struct AnalysisRequest {
65 pub kind: AnalysisKind,
66 pub target: AnalysisTarget,
67 pub parameters: Vec<(String, String)>,
68}
69
70#[derive(Clone, Debug, PartialEq)]
71pub struct AnalysisProvenance {
72 pub kind: AnalysisKind,
73 pub source_plots: Vec<String>,
74 pub parameters: Vec<(String, String)>,
75 pub notes: Vec<String>,
76}
77
78#[derive(Clone, Debug, PartialEq)]
79pub struct AnalysisReport {
80 pub title: String,
81 pub values: Vec<(String, String)>,
82}
83
84#[derive(Clone, Debug, PartialEq)]
85pub struct AnalysisTable {
86 pub columns: Vec<String>,
87 pub rows: Vec<Vec<String>>,
88}
89
90#[derive(Clone, Debug)]
91pub enum AnalysisOutput {
92 DerivedPlots {
93 plots: Vec<PlotSpec>,
94 provenance: AnalysisProvenance,
95 },
96 Report {
97 report: AnalysisReport,
98 provenance: AnalysisProvenance,
99 },
100 Table {
101 table: AnalysisTable,
102 provenance: AnalysisProvenance,
103 },
104 Composite {
105 plots: Vec<PlotSpec>,
106 reports: Vec<AnalysisReport>,
107 tables: Vec<AnalysisTable>,
108 diagnostics: Vec<Diagnostic>,
109 provenance: AnalysisProvenance,
110 },
111}
112
113#[derive(Clone, Copy, Debug, PartialEq, Eq)]
114pub enum SampleGroupsKind {
115 Curve,
116 Polyline,
117 InterpolationSource,
118}
119
120#[derive(Clone, Debug)]
121pub struct AnalysisError {
122 pub diagnostic: Diagnostic,
123}
124
125impl AnalysisError {
126 pub fn unsupported(message: impl Into<String>) -> Self {
127 Self {
128 diagnostic: Diagnostic::error(DiagnosticKind::Build, message),
129 }
130 }
131
132 pub fn invalid(message: impl Into<String>) -> Self {
133 Self {
134 diagnostic: Diagnostic::error(DiagnosticKind::Validation, message),
135 }
136 }
137}
138
139pub fn available_analyses(plot: &PlotSpec) -> Vec<AnalysisCapability> {
140 let metadata = plot.metadata();
141 capabilities_for_metadata(&metadata)
142}
143
144pub fn sample_groups(
145 plot: &PlotSpec,
146 kind: SampleGroupsKind,
147) -> Result<Vec<Vec<[f32; 3]>>, AnalysisError> {
148 match kind {
149 SampleGroupsKind::Curve => curve_sample_groups(plot),
150 SampleGroupsKind::Polyline => polyline_sample_groups(plot),
151 SampleGroupsKind::InterpolationSource => interpolation_source_groups(plot),
152 }
153}
154
155pub fn run_analysis(plot: &PlotSpec, request: &AnalysisRequest) -> Result<AnalysisOutput, AnalysisError> {
156 let params = parameter_map(&request.parameters);
157 let provenance = AnalysisProvenance {
158 kind: request.kind,
159 source_plots: vec![plot.name.clone()],
160 parameters: request.parameters.clone(),
161 notes: Vec::new(),
162 };
163
164 let plots = match request.kind {
165 AnalysisKind::ScalarSlice => vec![make_scalar_slice_plot(
166 plot,
167 parse_axis(params.get("axis").map(String::as_str)).unwrap_or(SliceAxis::Z),
168 params
169 .get("position")
170 .and_then(|value| value.parse::<f64>().ok()),
171 params
172 .get("contours")
173 .and_then(|value| value.parse::<usize>().ok()),
174 )?],
175 AnalysisKind::VectorSlice => vec![make_vector_slice_plot(
176 plot,
177 parse_axis(params.get("axis").map(String::as_str)).unwrap_or(SliceAxis::Z),
178 params
179 .get("position")
180 .and_then(|value| value.parse::<f64>().ok()),
181 )?],
182 AnalysisKind::GradientField => vec![make_gradient_plot(plot)?],
183 AnalysisKind::DivergenceField => vec![make_divergence_plot(plot)?],
184 AnalysisKind::CurlField => vec![make_curl_plot(plot)?],
185 AnalysisKind::DifferentiateCurve => vec![make_curve_derivative_plot(plot)?],
186 AnalysisKind::AxisDerivativeCurve => vec![make_axis_derivative_plot(
187 plot,
188 parse_axis_index(params.get("numerator_axis").map(String::as_str)).unwrap_or(1),
189 parse_axis_index(params.get("denominator_axis").map(String::as_str)).unwrap_or(0),
190 params.get("output_name").cloned(),
191 )?],
192 AnalysisKind::IntegralCurve => vec![make_curve_integral_plot(plot)?],
193 AnalysisKind::ArcLengthCurve => vec![make_curve_arc_length_plot(plot)?],
194 AnalysisKind::CurvatureCurve => vec![make_curve_curvature_plot(plot)?],
195 AnalysisKind::TangentField => vec![make_curve_tangent_plot(plot)?],
196 AnalysisKind::NormalField => vec![make_curve_normal_plot(plot)?],
197 AnalysisKind::BinormalField => vec![make_curve_binormal_plot(plot)?],
198 AnalysisKind::ExtractPoints => vec![make_extracted_points_plot(plot)?],
199 AnalysisKind::InterpolateCurve => make_interpolated_plots(
200 plot,
201 build_interpolation(¶ms),
202 params.get("output_name").cloned(),
203 )?,
204 AnalysisKind::SurfaceIntersection => {
205 return Err(AnalysisError::unsupported(
206 "Surface intersection remains a geometry-level app workflow.",
207 ));
208 }
209 };
210
211 Ok(AnalysisOutput::DerivedPlots { plots, provenance })
212}
213
214fn capabilities_for_metadata(metadata: &PlotMetadata) -> Vec<AnalysisCapability> {
215 let mut capabilities = Vec::new();
216
217 if metadata.style_caps.line {
218 capabilities.extend([
219 AnalysisCapability {
220 kind: AnalysisKind::InterpolateCurve,
221 target_kind: AnalysisTargetKind::SampledData,
222 output_kind: AnalysisOutputKind::PlotSpec,
223 parameters: vec![
224 "output_name",
225 "interpolation_kind",
226 "samples_per_segment",
227 "closed",
228 "smoothing_window",
229 ],
230 },
231 AnalysisCapability {
232 kind: AnalysisKind::DifferentiateCurve,
233 target_kind: AnalysisTargetKind::Definition,
234 output_kind: AnalysisOutputKind::PlotSpec,
235 parameters: vec![],
236 },
237 AnalysisCapability {
238 kind: AnalysisKind::AxisDerivativeCurve,
239 target_kind: AnalysisTargetKind::SampledData,
240 output_kind: AnalysisOutputKind::PlotSpec,
241 parameters: vec!["numerator_axis", "denominator_axis", "output_name"],
242 },
243 AnalysisCapability {
244 kind: AnalysisKind::IntegralCurve,
245 target_kind: AnalysisTargetKind::Definition,
246 output_kind: AnalysisOutputKind::PlotSpec,
247 parameters: vec![],
248 },
249 AnalysisCapability {
250 kind: AnalysisKind::ArcLengthCurve,
251 target_kind: AnalysisTargetKind::SampledData,
252 output_kind: AnalysisOutputKind::PlotSpec,
253 parameters: vec![],
254 },
255 AnalysisCapability {
256 kind: AnalysisKind::CurvatureCurve,
257 target_kind: AnalysisTargetKind::SampledData,
258 output_kind: AnalysisOutputKind::PlotSpec,
259 parameters: vec![],
260 },
261 AnalysisCapability {
262 kind: AnalysisKind::TangentField,
263 target_kind: AnalysisTargetKind::SampledData,
264 output_kind: AnalysisOutputKind::PlotSpec,
265 parameters: vec![],
266 },
267 AnalysisCapability {
268 kind: AnalysisKind::NormalField,
269 target_kind: AnalysisTargetKind::SampledData,
270 output_kind: AnalysisOutputKind::PlotSpec,
271 parameters: vec![],
272 },
273 AnalysisCapability {
274 kind: AnalysisKind::BinormalField,
275 target_kind: AnalysisTargetKind::SampledData,
276 output_kind: AnalysisOutputKind::PlotSpec,
277 parameters: vec![],
278 },
279 AnalysisCapability {
280 kind: AnalysisKind::ExtractPoints,
281 target_kind: AnalysisTargetKind::SampledData,
282 output_kind: AnalysisOutputKind::PlotSpec,
283 parameters: vec![],
284 },
285 ]);
286 }
287
288 if metadata.coordinate_semantics == crate::CoordinateSemantics::CartesianVolume {
289 capabilities.extend([
290 AnalysisCapability {
291 kind: AnalysisKind::ScalarSlice,
292 target_kind: AnalysisTargetKind::Definition,
293 output_kind: AnalysisOutputKind::PlotSpec,
294 parameters: vec!["axis", "position", "contours"],
295 },
296 AnalysisCapability {
297 kind: AnalysisKind::VectorSlice,
298 target_kind: AnalysisTargetKind::Definition,
299 output_kind: AnalysisOutputKind::PlotSpec,
300 parameters: vec!["axis", "position"],
301 },
302 ]);
303 }
304
305 if metadata.required_variables == ["x".to_string(), "y".to_string(), "z".to_string()] {
306 capabilities.extend([
307 AnalysisCapability {
308 kind: AnalysisKind::GradientField,
309 target_kind: AnalysisTargetKind::Definition,
310 output_kind: AnalysisOutputKind::PlotSpec,
311 parameters: vec![],
312 },
313 AnalysisCapability {
314 kind: AnalysisKind::DivergenceField,
315 target_kind: AnalysisTargetKind::Definition,
316 output_kind: AnalysisOutputKind::PlotSpec,
317 parameters: vec![],
318 },
319 AnalysisCapability {
320 kind: AnalysisKind::CurlField,
321 target_kind: AnalysisTargetKind::Definition,
322 output_kind: AnalysisOutputKind::PlotSpec,
323 parameters: vec![],
324 },
325 ]);
326 }
327
328 if metadata.supports_surface_intersection {
329 capabilities.push(AnalysisCapability {
330 kind: AnalysisKind::SurfaceIntersection,
331 target_kind: AnalysisTargetKind::PlotPair,
332 output_kind: AnalysisOutputKind::PlotSpec,
333 parameters: vec!["samples", "tolerance"],
334 });
335 }
336
337 capabilities
338}
339
340fn make_scalar_slice_plot(
341 source: &PlotSpec,
342 axis: SliceAxis,
343 position: Option<f64>,
344 contour_count: Option<usize>,
345) -> Result<PlotSpec, AnalysisError> {
346 let (expression, parameters) = match &source.definition {
347 crate::PlotDefinition::ExprVolume {
348 expression,
349 parameters,
350 ..
351 }
352 | crate::PlotDefinition::ExprIsosurface {
353 expression,
354 parameters,
355 ..
356 } => (expression.clone(), parameters.clone()),
357 _ => {
358 return Err(AnalysisError::unsupported(
359 "Scalar slices require a scalar volume or isosurface source.",
360 ));
361 }
362 };
363 Ok(PlotSpec {
364 name: format!("{} Slice {}", axis.label(), source.name),
365 visible: true,
366 domain: source.domain.clone(),
367 resolution: source.resolution,
368 style: PlotStyle {
369 colour_mode: ColourMode::ByAttribute {
370 name: "value".to_string(),
371 kind: viewport_lib::AttributeKind::Vertex,
372 },
373 two_sided: true,
374 ..source.style.clone()
375 },
376 definition: crate::PlotDefinition::ScalarSlice {
377 expression,
378 parameters,
379 axis,
380 position: position.unwrap_or_else(|| default_slice_position(&source.domain, axis)),
381 contour_values: evenly_spaced_isovalues(contour_count.unwrap_or(8)),
382 contour_style: PlotStyle {
383 colour_mode: ColourMode::Solid([1.0, 0.95, 0.35, 1.0]),
384 line_width: 2.0,
385 ..PlotStyle::default()
386 },
387 },
388 })
389}
390
391fn make_vector_slice_plot(
392 source: &PlotSpec,
393 axis: SliceAxis,
394 position: Option<f64>,
395) -> Result<PlotSpec, AnalysisError> {
396 let (expression, parameters) = match &source.definition {
397 crate::PlotDefinition::ExprVectorField {
398 expression,
399 parameters,
400 } => (expression.clone(), parameters.clone()),
401 _ => {
402 return Err(AnalysisError::unsupported(
403 "Vector slices require a vector field source.",
404 ));
405 }
406 };
407 Ok(PlotSpec {
408 name: format!("{} Slice {}", axis.label(), source.name),
409 visible: true,
410 domain: source.domain.clone(),
411 resolution: source.resolution,
412 style: source.style.clone(),
413 definition: crate::PlotDefinition::VectorSlice {
414 expression,
415 parameters,
416 axis,
417 position: position.unwrap_or_else(|| default_slice_position(&source.domain, axis)),
418 },
419 })
420}
421
422fn make_gradient_plot(source: &PlotSpec) -> Result<PlotSpec, AnalysisError> {
423 let (expression, parameters) = match &source.definition {
424 crate::PlotDefinition::ExprVolume {
425 expression,
426 parameters,
427 ..
428 }
429 | crate::PlotDefinition::ExprIsosurface {
430 expression,
431 parameters,
432 ..
433 } => (expression.clone(), parameters.clone()),
434 _ => {
435 return Err(AnalysisError::unsupported(
436 "Gradient plots require a scalar volume or isosurface source.",
437 ));
438 }
439 };
440 Ok(PlotSpec {
441 name: format!("Gradient {}", source.name),
442 visible: true,
443 domain: source.domain.clone(),
444 resolution: source.resolution,
445 style: PlotStyle {
446 colour_mode: ColourMode::ByAttribute {
447 name: "magnitude".to_string(),
448 kind: viewport_lib::AttributeKind::Vertex,
449 },
450 glyph_scale: 0.8,
451 shading: crate::ShadingMode::Unlit,
452 ..PlotStyle::default()
453 },
454 definition: crate::PlotDefinition::GradientField {
455 expression,
456 parameters,
457 },
458 })
459}
460
461fn make_divergence_plot(source: &PlotSpec) -> Result<PlotSpec, AnalysisError> {
462 let (expression, parameters) = match &source.definition {
463 crate::PlotDefinition::ExprVectorField {
464 expression,
465 parameters,
466 } => (expression.clone(), parameters.clone()),
467 _ => {
468 return Err(AnalysisError::unsupported(
469 "Divergence plots require a vector field source.",
470 ));
471 }
472 };
473 Ok(PlotSpec {
474 name: format!("Divergence {}", source.name),
475 visible: true,
476 domain: source.domain.clone(),
477 resolution: source.resolution,
478 style: PlotStyle {
479 opacity: 0.3,
480 transfer_function: Some(crate::TransferFunction {
481 opacity_scale: 0.4,
482 threshold: None,
483 }),
484 ..PlotStyle::default()
485 },
486 definition: crate::PlotDefinition::DivergenceField {
487 expression,
488 parameters,
489 vol_resolution: [64, 64, 64],
490 },
491 })
492}
493
494fn make_curl_plot(source: &PlotSpec) -> Result<PlotSpec, AnalysisError> {
495 let (expression, parameters) = match &source.definition {
496 crate::PlotDefinition::ExprVectorField {
497 expression,
498 parameters,
499 } => (expression.clone(), parameters.clone()),
500 _ => {
501 return Err(AnalysisError::unsupported(
502 "Curl plots require a vector field source.",
503 ));
504 }
505 };
506 Ok(PlotSpec {
507 name: format!("Curl {}", source.name),
508 visible: true,
509 domain: source.domain.clone(),
510 resolution: source.resolution,
511 style: PlotStyle {
512 colour_mode: ColourMode::ByAttribute {
513 name: "magnitude".to_string(),
514 kind: viewport_lib::AttributeKind::Vertex,
515 },
516 glyph_scale: 0.8,
517 shading: crate::ShadingMode::Unlit,
518 ..PlotStyle::default()
519 },
520 definition: crate::PlotDefinition::CurlField {
521 expression,
522 parameters,
523 },
524 })
525}
526
527fn make_curve_derivative_plot(source: &PlotSpec) -> Result<PlotSpec, AnalysisError> {
528 let groups = curve_sample_groups(source)?;
529 let derived_groups = match &source.definition {
530 crate::PlotDefinition::ExprCartesianLine {
531 dep_var, ind_var, ..
532 } => groups
533 .iter()
534 .map(|group| derivative_cartesian_line_group(group, dep_var.as_str(), ind_var.as_str()))
535 .filter(|group| group.len() >= 2)
536 .collect(),
537 _ => groups
538 .iter()
539 .map(|group| derivative_curve_group(group))
540 .filter(|group| group.len() >= 2)
541 .collect(),
542 };
543 derived_polyline_plot(
544 source,
545 format!("Derivative {}", source.name),
546 [1.0, 0.55, 0.25, 1.0],
547 derived_groups,
548 )
549}
550
551fn make_curve_tangent_plot(source: &PlotSpec) -> Result<PlotSpec, AnalysisError> {
552 let groups = curve_sample_groups(source)?;
553 let derived_groups = groups
554 .iter()
555 .map(|group| tangent_curve_group(group))
556 .filter(|group| group.len() >= 2)
557 .collect();
558 derived_polyline_plot(
559 source,
560 format!("Tangent {}", source.name),
561 [0.25, 0.85, 0.45, 1.0],
562 derived_groups,
563 )
564}
565
566fn make_curve_integral_plot(source: &PlotSpec) -> Result<PlotSpec, AnalysisError> {
567 let groups = curve_sample_groups(source)?;
568 let derived_groups = match &source.definition {
569 crate::PlotDefinition::ExprCartesianLine {
570 dep_var, ind_var, ..
571 } => groups
572 .iter()
573 .map(|group| integral_cartesian_line_group(group, dep_var.as_str(), ind_var.as_str()))
574 .filter(|group| group.len() >= 2)
575 .collect(),
576 _ => groups
577 .iter()
578 .map(|group| integral_curve_group(group))
579 .filter(|group| group.len() >= 2)
580 .collect(),
581 };
582 derived_polyline_plot(
583 source,
584 format!("Integral {}", source.name),
585 [0.45, 0.7, 1.0, 1.0],
586 derived_groups,
587 )
588}
589
590fn make_curve_arc_length_plot(source: &PlotSpec) -> Result<PlotSpec, AnalysisError> {
591 let groups = curve_sample_groups(source)?;
592 let derived_groups = match &source.definition {
593 crate::PlotDefinition::ExprCartesianLine {
594 dep_var, ind_var, ..
595 } => groups
596 .iter()
597 .map(|group| {
598 scalar_plot_cartesian_line_group(
599 group,
600 dep_var.as_str(),
601 ind_var.as_str(),
602 &cumulative_arc_lengths(group),
603 )
604 })
605 .filter(|group| group.len() >= 2)
606 .collect(),
607 _ => groups
608 .iter()
609 .map(|group| scalar_curve_group(group, &cumulative_arc_lengths(group)))
610 .filter(|group| group.len() >= 2)
611 .collect(),
612 };
613 derived_polyline_plot(
614 source,
615 format!("Arc Length {}", source.name),
616 [0.95, 0.85, 0.3, 1.0],
617 derived_groups,
618 )
619}
620
621fn make_curve_curvature_plot(source: &PlotSpec) -> Result<PlotSpec, AnalysisError> {
622 let groups = curve_sample_groups(source)?;
623 let derived_groups = match &source.definition {
624 crate::PlotDefinition::ExprCartesianLine {
625 dep_var, ind_var, ..
626 } => groups
627 .iter()
628 .map(|group| {
629 scalar_plot_cartesian_line_group(
630 group,
631 dep_var.as_str(),
632 ind_var.as_str(),
633 &curvature_values(group),
634 )
635 })
636 .filter(|group| group.len() >= 2)
637 .collect(),
638 _ => groups
639 .iter()
640 .map(|group| scalar_curve_group(group, &curvature_values(group)))
641 .filter(|group| group.len() >= 2)
642 .collect(),
643 };
644 derived_polyline_plot(
645 source,
646 format!("Curvature {}", source.name),
647 [0.8, 0.45, 1.0, 1.0],
648 derived_groups,
649 )
650}
651
652fn make_curve_normal_plot(source: &PlotSpec) -> Result<PlotSpec, AnalysisError> {
653 let groups = curve_sample_groups(source)?;
654 let derived_groups = groups
655 .iter()
656 .map(|group| normal_curve_group(group))
657 .filter(|group| group.len() >= 2)
658 .collect();
659 derived_polyline_plot(
660 source,
661 format!("Normal {}", source.name),
662 [0.3, 0.8, 1.0, 1.0],
663 derived_groups,
664 )
665}
666
667fn make_curve_binormal_plot(source: &PlotSpec) -> Result<PlotSpec, AnalysisError> {
668 let groups = curve_sample_groups(source)?;
669 let derived_groups = groups
670 .iter()
671 .map(|group| binormal_curve_group(group))
672 .filter(|group| group.len() >= 2)
673 .collect();
674 derived_polyline_plot(
675 source,
676 format!("Binormal {}", source.name),
677 [1.0, 0.45, 0.65, 1.0],
678 derived_groups,
679 )
680}
681
682fn make_axis_derivative_plot(
683 source: &PlotSpec,
684 numerator_axis: usize,
685 denominator_axis: usize,
686 output_name: Option<String>,
687) -> Result<PlotSpec, AnalysisError> {
688 if numerator_axis == denominator_axis {
689 return Err(AnalysisError::invalid(
690 "Numerator and denominator axes must be different.",
691 ));
692 }
693 let groups = curve_sample_groups(source)?;
694 let derived_groups: Vec<Vec<[f32; 3]>> = groups
695 .iter()
696 .map(|group| axis_derivative_group(group, numerator_axis, denominator_axis))
697 .filter(|group| group.len() >= 2)
698 .collect();
699 if derived_groups.is_empty() {
700 return Err(AnalysisError::invalid(
701 "Could not compute an axis derivative from the selected curve.",
702 ));
703 }
704 derived_polyline_plot(
705 source,
706 output_name.unwrap_or_else(|| {
707 format!(
708 "d{}/d{} {}",
709 axis_name(numerator_axis),
710 axis_name(denominator_axis),
711 source.name
712 )
713 }),
714 [1.0, 0.7, 0.25, 1.0],
715 derived_groups,
716 )
717}
718
719fn make_extracted_points_plot(source: &PlotSpec) -> Result<PlotSpec, AnalysisError> {
720 let positions = polyline_sample_groups(source)?.into_iter().flatten().collect::<Vec<_>>();
721 Ok(PlotSpec {
722 name: format!("Points from {}", source.name),
723 visible: true,
724 domain: source.domain.clone(),
725 resolution: source.resolution,
726 style: PlotStyle {
727 colour_mode: ColourMode::Solid([0.35, 0.85, 1.0, 1.0]),
728 point_size: 8.0,
729 ..PlotStyle::default()
730 },
731 definition: crate::PlotDefinition::PointAnnotations {
732 points: make_point_annotations(&positions, "Point"),
733 show_labels: false,
734 },
735 })
736}
737
738fn make_interpolated_plots(
739 source: &PlotSpec,
740 interpolation: CurveInterpolation,
741 output_name: Option<String>,
742) -> Result<Vec<PlotSpec>, AnalysisError> {
743 let groups = interpolation_source_groups(source)?;
744 if groups.is_empty() || groups.iter().all(Vec::is_empty) {
745 return Err(AnalysisError::invalid(
746 "The selected plot does not have usable point samples.",
747 ));
748 }
749 if groups.iter().all(|group| group.len() < 2) {
750 return Err(AnalysisError::invalid(
751 "At least two points are required to interpolate a curve.",
752 ));
753 }
754
755 let base_name = output_name.unwrap_or_else(|| format!("Interpolated {}", source.name));
756 let style = PlotStyle {
757 colour_mode: ColourMode::Solid([0.95, 0.7, 0.2, 1.0]),
758 line_width: 2.5,
759 ..PlotStyle::default()
760 };
761
762 if groups.len() == 1 {
763 let points = groups.into_iter().next().unwrap_or_default();
764 return Ok(vec![PlotSpec {
765 name: base_name,
766 visible: true,
767 domain: source.domain.clone(),
768 resolution: source.resolution,
769 style,
770 definition: crate::PlotDefinition::InterpolatedCurve {
771 points,
772 interpolation,
773 },
774 }]);
775 }
776
777 Ok(groups
778 .into_iter()
779 .enumerate()
780 .filter(|(_, group)| group.len() >= 2)
781 .map(|(index, group)| PlotSpec {
782 name: format!("{base_name} {}", index + 1),
783 visible: true,
784 domain: source.domain.clone(),
785 resolution: source.resolution,
786 style: style.clone(),
787 definition: crate::PlotDefinition::InterpolatedCurve {
788 points: group,
789 interpolation,
790 },
791 })
792 .collect())
793}
794
795fn interpolation_source_groups(plot: &PlotSpec) -> Result<Vec<Vec<[f32; 3]>>, AnalysisError> {
796 match &plot.definition {
797 crate::PlotDefinition::PointAnnotations { points, .. } => Ok(vec![points
798 .iter()
799 .map(|point| point.position)
800 .collect()]),
801 crate::PlotDefinition::ExprCurve { .. }
802 | crate::PlotDefinition::ExprCartesianLine { .. }
803 | crate::PlotDefinition::HelixCurve => curve_sample_groups(plot),
804 crate::PlotDefinition::ImportedTable { definition } => match definition.validate() {
805 Ok(TableDataSet::Curve { groups, .. }) => Ok(groups
806 .iter()
807 .map(|group| group.iter().map(|point| point.to_array()).collect())
808 .collect()),
809 Ok(TableDataSet::Scatter { points, .. }) => {
810 Ok(vec![points.iter().map(|point| point.to_array()).collect()])
811 }
812 Ok(_) => Err(AnalysisError::unsupported(
813 "Interpolation is not available for this imported table target.",
814 )),
815 Err(errors) => Err(table_errors(errors)),
816 },
817 crate::PlotDefinition::DerivedPolylineGroups { groups } => Ok(groups.clone()),
818 crate::PlotDefinition::InterpolatedCurve { points, .. } => Ok(vec![points.clone()]),
819 _ => Err(AnalysisError::unsupported(
820 "Interpolation is available for point and ordered sample plots.",
821 )),
822 }
823}
824
825fn polyline_sample_groups(plot: &PlotSpec) -> Result<Vec<Vec<[f32; 3]>>, AnalysisError> {
826 match &plot.definition {
827 crate::PlotDefinition::ExprCurve { .. }
828 | crate::PlotDefinition::ExprCartesianLine { .. }
829 | crate::PlotDefinition::HelixCurve => curve_sample_groups(plot),
830 crate::PlotDefinition::ImportedTable { definition } => match definition.validate() {
831 Ok(TableDataSet::Curve { groups, .. }) => Ok(groups
832 .iter()
833 .map(|group| group.iter().map(|point| point.to_array()).collect())
834 .collect()),
835 Ok(_) => Err(AnalysisError::unsupported(
836 "Point extraction is only available for imported curve tables.",
837 )),
838 Err(errors) => Err(table_errors(errors)),
839 },
840 crate::PlotDefinition::DerivedPolylineGroups { groups } => Ok(groups.clone()),
841 crate::PlotDefinition::InterpolatedCurve {
842 points,
843 interpolation,
844 } => {
845 let sampled = sample_curve_points(
846 &points.iter().map(|point| Vec3::from_array(*point)).collect::<Vec<_>>(),
847 *interpolation,
848 );
849 Ok(vec![sampled.into_iter().map(|point| point.to_array()).collect()])
850 }
851 _ => Err(AnalysisError::unsupported(
852 "Point extraction is available for polyline and interpolated curve plots.",
853 )),
854 }
855}
856
857fn curve_sample_groups(plot: &PlotSpec) -> Result<Vec<Vec<[f32; 3]>>, AnalysisError> {
858 match &plot.definition {
859 crate::PlotDefinition::HelixCurve => {
860 let steps = plot.resolution.u.max(2) as usize;
861 let points = (0..steps)
862 .map(|i| {
863 let t = 20.0 * PI * i as f64 / (steps - 1) as f64;
864 Vec3::new((t.cos() * 3.0) as f32, (t.sin() * 3.0) as f32, (t * 0.15) as f32)
865 .to_array()
866 })
867 .collect();
868 Ok(vec![points])
869 }
870 crate::PlotDefinition::ExprCurve {
871 expression,
872 parameters,
873 t_range,
874 } => {
875 let parsed = parse_curve_expr(expression).map_err(parse_error)?;
876 let steps = plot.resolution.u.max(2) as usize;
877 let (t0, t1) = *t_range;
878 let points = (0..steps)
879 .map(|i| {
880 let t = t0 + (i as f64 / (steps - 1) as f64) * (t1 - t0);
881 let p = eval_curve_point(&parsed, t, parameters);
882 Vec3::new(p.x as f32, p.y as f32, p.z as f32).to_array()
883 })
884 .collect();
885 Ok(vec![points])
886 }
887 crate::PlotDefinition::ExprCartesianLine {
888 dep_var,
889 ind_var,
890 expression,
891 parameters,
892 } => {
893 let parsed = parse_expr_with_vars(expression, &[ind_var.as_str()]).map_err(parse_error)?;
894 let steps = plot.resolution.u.max(2) as usize;
895 let (t0, t1) = (*plot.domain.x.start(), *plot.domain.x.end());
896 let dep = dep_var.clone();
897 let ind = ind_var.clone();
898 let points = (0..steps)
899 .map(|i| {
900 let t = t0 + (i as f64 / (steps - 1) as f64) * (t1 - t0);
901 let vars: Vec<(&str, f64)> = parameters
902 .iter()
903 .map(|(n, v)| (n.as_str(), *v))
904 .chain(std::iter::once((ind.as_str(), t)))
905 .collect();
906 let val = eval_with_vars(&parsed, &vars);
907 cartesian_line_point(dep.as_str(), ind.as_str(), t as f32, val as f32).to_array()
908 })
909 .collect();
910 Ok(vec![points])
911 }
912 crate::PlotDefinition::ImportedTable { definition } => match definition.validate() {
913 Ok(TableDataSet::Curve { groups, .. }) => Ok(groups
914 .iter()
915 .map(|group| group.iter().map(|point| point.to_array()).collect())
916 .collect()),
917 Ok(_) => Err(AnalysisError::unsupported(
918 "Curve calculus tools require curve-like sample data.",
919 )),
920 Err(errors) => Err(table_errors(errors)),
921 },
922 crate::PlotDefinition::DerivedPolylineGroups { groups } => Ok(groups.clone()),
923 crate::PlotDefinition::InterpolatedCurve {
924 points,
925 interpolation,
926 } => {
927 let sampled = sample_curve_points(
928 &points.iter().map(|point| Vec3::from_array(*point)).collect::<Vec<_>>(),
929 *interpolation,
930 );
931 Ok(vec![sampled.into_iter().map(|point| point.to_array()).collect()])
932 }
933 _ => Err(AnalysisError::unsupported(
934 "Curve calculus tools are available for curve and polyline plots.",
935 )),
936 }
937}
938
939fn derived_polyline_plot(
940 source: &PlotSpec,
941 name: String,
942 color: [f32; 4],
943 groups: Vec<Vec<[f32; 3]>>,
944) -> Result<PlotSpec, AnalysisError> {
945 if groups.is_empty() {
946 return Err(AnalysisError::invalid(
947 "The selected plot did not produce enough samples for this analysis.",
948 ));
949 }
950 Ok(PlotSpec {
951 name,
952 visible: true,
953 domain: source.domain.clone(),
954 resolution: source.resolution,
955 style: PlotStyle {
956 colour_mode: ColourMode::Solid(color),
957 line_width: 2.25,
958 ..PlotStyle::default()
959 },
960 definition: crate::PlotDefinition::DerivedPolylineGroups { groups },
961 })
962}
963
964fn parameter_map(parameters: &[(String, String)]) -> HashMap<String, String> {
965 parameters.iter().cloned().collect()
966}
967
968fn build_interpolation(params: &HashMap<String, String>) -> CurveInterpolation {
969 CurveInterpolation {
970 kind: parse_interpolation_kind(params.get("interpolation_kind").map(String::as_str))
971 .unwrap_or(CurveInterpolationKind::Linear),
972 samples_per_segment: params
973 .get("samples_per_segment")
974 .and_then(|value| value.parse::<u32>().ok())
975 .unwrap_or(1),
976 closed: params
977 .get("closed")
978 .is_some_and(|value| matches!(value.as_str(), "1" | "true" | "yes")),
979 smoothing_window: normalized_window_value(
980 params
981 .get("smoothing_window")
982 .and_then(|value| value.parse::<u32>().ok())
983 .unwrap_or(5),
984 ),
985 }
986}
987
988fn parse_interpolation_kind(value: Option<&str>) -> Option<CurveInterpolationKind> {
989 match value? {
990 "linear" => Some(CurveInterpolationKind::Linear),
991 "catmull_rom" => Some(CurveInterpolationKind::CatmullRom),
992 "centripetal_catmull_rom" => Some(CurveInterpolationKind::CentripetalCatmullRom),
993 "moving_average" => Some(CurveInterpolationKind::MovingAverage),
994 "savitzky_golay" => Some(CurveInterpolationKind::SavitzkyGolay),
995 _ => None,
996 }
997}
998
999fn parse_axis(value: Option<&str>) -> Option<SliceAxis> {
1000 match value?.to_ascii_lowercase().as_str() {
1001 "x" => Some(SliceAxis::X),
1002 "y" => Some(SliceAxis::Y),
1003 "z" => Some(SliceAxis::Z),
1004 _ => None,
1005 }
1006}
1007
1008fn parse_axis_index(value: Option<&str>) -> Option<usize> {
1009 match value?.to_ascii_lowercase().as_str() {
1010 "x" | "0" => Some(0),
1011 "y" | "1" => Some(1),
1012 "z" | "2" => Some(2),
1013 _ => None,
1014 }
1015}
1016
1017fn evenly_spaced_isovalues(count: usize) -> Vec<f32> {
1018 let count = count.max(1);
1019 if count == 1 {
1020 return vec![0.0];
1021 }
1022 (0..count)
1023 .map(|i| -0.9 + 1.8 * i as f32 / (count - 1) as f32)
1024 .collect()
1025}
1026
1027fn parse_error(error: impl ToString) -> AnalysisError {
1028 AnalysisError {
1029 diagnostic: Diagnostic::error(DiagnosticKind::Parse, error.to_string()),
1030 }
1031}
1032
1033fn table_errors(errors: Vec<crate::TableValidationError>) -> AnalysisError {
1034 let summary = errors
1035 .into_iter()
1036 .map(|error| error.display())
1037 .collect::<Vec<_>>()
1038 .join("; ");
1039 AnalysisError::invalid(summary)
1040}
1041
1042fn make_point_annotations(points: &[[f32; 3]], prefix: &str) -> Vec<PointAnnotation> {
1043 points
1044 .iter()
1045 .enumerate()
1046 .map(|(index, position)| PointAnnotation {
1047 position: *position,
1048 label: format!("{prefix} {}", index + 1),
1049 })
1050 .collect()
1051}
1052
1053fn axis_name(axis: usize) -> &'static str {
1054 match axis {
1055 0 => "x",
1056 1 => "y",
1057 _ => "z",
1058 }
1059}
1060
1061fn normalized_window_value(window: u32) -> u32 {
1062 let mut normalized = window.max(3);
1063 if normalized % 2 == 0 {
1064 normalized += 1;
1065 }
1066 normalized
1067}
1068
1069fn derivative_curve_group(group: &[[f32; 3]]) -> Vec<[f32; 3]> {
1070 if group.len() < 2 {
1071 return Vec::new();
1072 }
1073 (0..group.len())
1074 .map(|index| finite_difference(group, index).to_array())
1075 .collect()
1076}
1077
1078fn tangent_curve_group(group: &[[f32; 3]]) -> Vec<[f32; 3]> {
1079 if group.len() < 2 {
1080 return Vec::new();
1081 }
1082 (0..group.len())
1083 .map(|index| finite_difference(group, index).normalize_or_zero().to_array())
1084 .collect()
1085}
1086
1087fn finite_difference(group: &[[f32; 3]], index: usize) -> Vec3 {
1088 let current = Vec3::from_array(group[index]);
1089 if index == 0 {
1090 let next = Vec3::from_array(group[1]);
1091 return next - current;
1092 }
1093 if index + 1 == group.len() {
1094 let prev = Vec3::from_array(group[index - 1]);
1095 return current - prev;
1096 }
1097 let prev = Vec3::from_array(group[index - 1]);
1098 let next = Vec3::from_array(group[index + 1]);
1099 (next - prev) * 0.5
1100}
1101
1102fn integral_curve_group(group: &[[f32; 3]]) -> Vec<[f32; 3]> {
1103 if group.len() < 2 {
1104 return Vec::new();
1105 }
1106 let mut out = Vec::with_capacity(group.len());
1107 let mut accum = Vec3::ZERO;
1108 out.push(accum.to_array());
1109 let dt = 1.0_f32 / (group.len() - 1) as f32;
1110 for pair in group.windows(2) {
1111 let a = Vec3::from_array(pair[0]);
1112 let b = Vec3::from_array(pair[1]);
1113 accum += (a + b) * 0.5 * dt;
1114 out.push(accum.to_array());
1115 }
1116 out
1117}
1118
1119fn cumulative_arc_lengths(group: &[[f32; 3]]) -> Vec<f32> {
1120 if group.is_empty() {
1121 return Vec::new();
1122 }
1123 let mut out = Vec::with_capacity(group.len());
1124 let mut total = 0.0_f32;
1125 out.push(total);
1126 for pair in group.windows(2) {
1127 let a = Vec3::from_array(pair[0]);
1128 let b = Vec3::from_array(pair[1]);
1129 total += b.distance(a);
1130 out.push(total);
1131 }
1132 out
1133}
1134
1135fn curvature_values(group: &[[f32; 3]]) -> Vec<f32> {
1136 if group.len() < 3 {
1137 return vec![0.0; group.len()];
1138 }
1139 let mut values = vec![0.0_f32; group.len()];
1140 for index in 1..(group.len() - 1) {
1141 let a = Vec3::from_array(group[index - 1]);
1142 let b = Vec3::from_array(group[index]);
1143 let c = Vec3::from_array(group[index + 1]);
1144 let ab = b - a;
1145 let bc = c - b;
1146 let ac = c - a;
1147 let denom = ab.length() * bc.length() * ac.length();
1148 if denom > 1.0e-6 {
1149 values[index] = 2.0 * ab.cross(ac).length() / denom;
1150 }
1151 }
1152 values[0] = values[1];
1153 values[group.len() - 1] = values[group.len() - 2];
1154 values
1155}
1156
1157fn scalar_curve_group(group: &[[f32; 3]], values: &[f32]) -> Vec<[f32; 3]> {
1158 if group.len() != values.len() || group.len() < 2 {
1159 return Vec::new();
1160 }
1161 let denom = (group.len() - 1) as f32;
1162 values
1163 .iter()
1164 .enumerate()
1165 .map(|(index, value)| Vec3::new(index as f32 / denom, *value, 0.0).to_array())
1166 .collect()
1167}
1168
1169fn scalar_plot_cartesian_line_group(
1170 group: &[[f32; 3]],
1171 dep_var: &str,
1172 ind_var: &str,
1173 values: &[f32],
1174) -> Vec<[f32; 3]> {
1175 if group.len() != values.len() || group.len() < 2 {
1176 return Vec::new();
1177 }
1178 group
1179 .iter()
1180 .zip(values.iter())
1181 .filter_map(|(point, value)| {
1182 let independent = cartesian_axis_value(Vec3::from_array(*point), ind_var)?;
1183 Some(cartesian_line_point(dep_var, ind_var, independent, *value).to_array())
1184 })
1185 .collect()
1186}
1187
1188fn axis_derivative_group(
1189 group: &[[f32; 3]],
1190 numerator_axis: usize,
1191 denominator_axis: usize,
1192) -> Vec<[f32; 3]> {
1193 if group.len() < 2 {
1194 return Vec::new();
1195 }
1196 (0..group.len())
1197 .filter_map(|index| {
1198 let point = Vec3::from_array(group[index]);
1199 let denominator = axis_value(point, denominator_axis);
1200 let derivative = axis_derivative_value(group, index, numerator_axis, denominator_axis)?;
1201 Some(Vec3::new(denominator, derivative, 0.0).to_array())
1202 })
1203 .collect()
1204}
1205
1206fn derivative_cartesian_line_group(
1207 group: &[[f32; 3]],
1208 dep_var: &str,
1209 ind_var: &str,
1210) -> Vec<[f32; 3]> {
1211 if group.len() < 2 {
1212 return Vec::new();
1213 }
1214 (0..group.len())
1215 .filter_map(|index| {
1216 let current = Vec3::from_array(group[index]);
1217 let current_ind = cartesian_axis_value(current, ind_var)?;
1218 let derivative = scalar_derivative(group, index, dep_var, ind_var)?;
1219 Some(cartesian_line_point(dep_var, ind_var, current_ind, derivative).to_array())
1220 })
1221 .collect()
1222}
1223
1224fn scalar_derivative(
1225 group: &[[f32; 3]],
1226 index: usize,
1227 dep_var: &str,
1228 ind_var: &str,
1229) -> Option<f32> {
1230 if group.len() < 2 {
1231 return None;
1232 }
1233 let (a, b) = if index == 0 {
1234 (0, 1)
1235 } else if index + 1 == group.len() {
1236 (group.len() - 2, group.len() - 1)
1237 } else {
1238 (index - 1, index + 1)
1239 };
1240 let pa = Vec3::from_array(group[a]);
1241 let pb = Vec3::from_array(group[b]);
1242 let da = cartesian_axis_value(pa, dep_var)?;
1243 let db = cartesian_axis_value(pb, dep_var)?;
1244 let ia = cartesian_axis_value(pa, ind_var)?;
1245 let ib = cartesian_axis_value(pb, ind_var)?;
1246 let denom = ib - ia;
1247 if denom.abs() <= 1.0e-6 {
1248 return Some(0.0);
1249 }
1250 Some((db - da) / denom)
1251}
1252
1253fn axis_derivative_value(
1254 group: &[[f32; 3]],
1255 index: usize,
1256 numerator_axis: usize,
1257 denominator_axis: usize,
1258) -> Option<f32> {
1259 if group.len() < 2 {
1260 return None;
1261 }
1262 let (a, b) = if index == 0 {
1263 (0, 1)
1264 } else if index + 1 == group.len() {
1265 (group.len() - 2, group.len() - 1)
1266 } else {
1267 (index - 1, index + 1)
1268 };
1269 let pa = Vec3::from_array(group[a]);
1270 let pb = Vec3::from_array(group[b]);
1271 let na = axis_value(pa, numerator_axis);
1272 let nb = axis_value(pb, numerator_axis);
1273 let da = axis_value(pa, denominator_axis);
1274 let db = axis_value(pb, denominator_axis);
1275 let denom = db - da;
1276 if denom.abs() <= 1.0e-6 {
1277 return Some(0.0);
1278 }
1279 Some((nb - na) / denom)
1280}
1281
1282fn cartesian_axis_value(point: Vec3, axis: &str) -> Option<f32> {
1283 match axis {
1284 "x" => Some(point.x),
1285 "y" => Some(point.y),
1286 "z" => Some(point.z),
1287 _ => None,
1288 }
1289}
1290
1291fn axis_value(point: Vec3, axis: usize) -> f32 {
1292 match axis {
1293 0 => point.x,
1294 1 => point.y,
1295 _ => point.z,
1296 }
1297}
1298
1299fn cartesian_line_point(dep_var: &str, ind_var: &str, independent: f32, dependent: f32) -> Vec3 {
1300 match (dep_var, ind_var) {
1301 ("y", "x") => Vec3::new(independent, dependent, 0.0),
1302 ("z", "x") => Vec3::new(independent, 0.0, dependent),
1303 ("z", "y") => Vec3::new(0.0, independent, dependent),
1304 ("x", "y") => Vec3::new(dependent, independent, 0.0),
1305 ("x", "z") => Vec3::new(dependent, 0.0, independent),
1306 ("y", "z") => Vec3::new(0.0, dependent, independent),
1307 _ => Vec3::new(independent, dependent, 0.0),
1308 }
1309}
1310
1311fn integral_cartesian_line_group(
1312 group: &[[f32; 3]],
1313 dep_var: &str,
1314 ind_var: &str,
1315) -> Vec<[f32; 3]> {
1316 if group.len() < 2 {
1317 return Vec::new();
1318 }
1319 let mut out = Vec::with_capacity(group.len());
1320 let start_independent = cartesian_axis_value(Vec3::from_array(group[0]), ind_var).unwrap_or(0.0);
1321 let mut accum = 0.0_f32;
1322 out.push(cartesian_line_point(dep_var, ind_var, start_independent, accum).to_array());
1323 for pair in group.windows(2) {
1324 let a = Vec3::from_array(pair[0]);
1325 let b = Vec3::from_array(pair[1]);
1326 let ia = match cartesian_axis_value(a, ind_var) {
1327 Some(value) => value,
1328 None => continue,
1329 };
1330 let ib = match cartesian_axis_value(b, ind_var) {
1331 Some(value) => value,
1332 None => continue,
1333 };
1334 let da = match cartesian_axis_value(a, dep_var) {
1335 Some(value) => value,
1336 None => continue,
1337 };
1338 let db = match cartesian_axis_value(b, dep_var) {
1339 Some(value) => value,
1340 None => continue,
1341 };
1342 accum += (da + db) * 0.5 * (ib - ia);
1343 out.push(cartesian_line_point(dep_var, ind_var, ib, accum).to_array());
1344 }
1345 out
1346}
1347
1348fn normal_curve_group(group: &[[f32; 3]]) -> Vec<[f32; 3]> {
1349 if group.len() < 3 {
1350 return Vec::new();
1351 }
1352 let tangents = tangent_vectors(group);
1353 tangents
1354 .iter()
1355 .enumerate()
1356 .map(|(index, _)| {
1357 let dt = if index == 0 {
1358 tangents[1] - tangents[0]
1359 } else if index + 1 == tangents.len() {
1360 tangents[index] - tangents[index - 1]
1361 } else {
1362 (tangents[index + 1] - tangents[index - 1]) * 0.5
1363 };
1364 dt.normalize_or_zero().to_array()
1365 })
1366 .collect()
1367}
1368
1369fn binormal_curve_group(group: &[[f32; 3]]) -> Vec<[f32; 3]> {
1370 if group.len() < 3 {
1371 return Vec::new();
1372 }
1373 let tangents = tangent_vectors(group);
1374 let normals = normal_curve_group(group);
1375 tangents
1376 .iter()
1377 .zip(normals.iter())
1378 .map(|(tangent, normal)| tangent.cross(Vec3::from_array(*normal)).normalize_or_zero().to_array())
1379 .collect()
1380}
1381
1382fn tangent_vectors(group: &[[f32; 3]]) -> Vec<Vec3> {
1383 if group.len() < 2 {
1384 return Vec::new();
1385 }
1386 (0..group.len())
1387 .map(|index| finite_difference(group, index).normalize_or_zero())
1388 .collect()
1389}