Skip to main content

poincare_lib/
analysis.rs

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(&params),
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}