Skip to main content

energyml_section/
lib.rs

1//! The section view - long and cross sections of a model along a trace.
2//!
3//! ADR-0015 is this crate's specification: every
4//! non-trivial choice in the code follows the decision recorded there.
5//!
6//! # Three stages, three costs
7//!
8//! ```text
9//! inputs ──► read (per package) ──► assemble ──► Catalogue      expensive, cached
10//!                                                 │
11//!                                       View ──► layout ──► Layout ──► svg::write
12//! ```
13//!
14//! 1. [`read::contribution`] reads one package's objects - wells, surfaces,
15//!    grids - reprojected into the scene's system and cut along every section
16//!    trace. It is the only stage that touches a store.
17//! 2. [`assemble::assemble`] joins the contributions of every package into a
18//!    [`model::Catalogue`]: wells projected on each section, frames and
19//!    trajectories of one wellbore folded into one line, every object's fate.
20//! 3. [`layout::layout`] turns a catalogue and a [`model::View`] into a
21//!    [`layout::Layout`] - scales, ticks, fills, labels, all in numbers - and
22//!    [`svg::write`] writes it. Nothing is read or recomputed here: the front end
23//!    redraws on every view change, the export draws the same view.
24//!
25//! The same split as `energyml-sheet` (`Board` → `layout` → `svg::write`), the
26//! precedent of a drawing computed in Rust.
27//!
28//! # No native dependency
29//!
30//! wasm is a hard requirement (see ADR-0007): everything here is
31//! computation over a `ValueSource`, and the SVG is a `String`.
32
33pub mod assemble;
34pub mod crosses;
35pub mod erosion;
36pub mod inputs;
37pub mod layout;
38pub mod markers;
39pub mod model;
40pub mod paint;
41pub mod panels;
42pub mod png;
43pub mod read;
44pub mod spatial;
45pub mod style;
46pub mod svg;
47
48use crate::inputs::InputsError;
49use crate::model::{Asked, Catalogue, Contribution, Inputs, SectionTrace, View};
50
51/// The whole calculation for one call: the sections' traces, one contribution
52/// per package - read by `read_one`, which is the face's, since only the face
53/// holds the packages - and the catalogue.
54///
55/// The faces (the CLI, wasm) call this rather than chaining the stages
56/// themselves: no face carries logic (ADR-0012).
57pub fn compute(
58    inputs: &Inputs,
59    mut read_one: impl FnMut(&Asked, &[SectionTrace]) -> Contribution,
60) -> Result<Catalogue, InputsError> {
61    let (sections, _) = crosses::traces(inputs)?;
62    let contributions: Vec<Contribution> = inputs
63        .objects
64        .iter()
65        .map(|asked| read_one(asked, &sections))
66        .collect();
67    assemble::assemble(inputs, &contributions)
68}
69
70/// One package handed to [`compute_packages`]: the store id its objects are
71/// asked under (`Asked::store`), where they are read, what the face already
72/// knows of it, and - when the face keeps one - its cache.
73pub struct Opened<'a> {
74    /// The id `Asked::store` names it by (`s1`, `s2`, … in the front end).
75    pub store: String,
76    /// Where its objects and arrays are read.
77    pub source: &'a dyn energyml_domain::ArraySource,
78    /// Its colour sets, index and colour resolver - the face's own when it has them.
79    pub context: read::PackageContext<'a>,
80    /// What it keeps between two calculations; `None`: nothing is kept.
81    pub cache: Option<&'a mut read::PackageCache>,
82}
83
84/// The whole calculation over **one or several packages**, each with its own
85/// selection (`Inputs::objects`, one `Asked` per package): every package read
86/// through its cache, the contributions merged by [`assemble::assemble`] into
87/// one catalogue - the wells of all packages projected on the same sections,
88/// their surfaces cut along the same traces, in the scene's one system.
89///
90/// A selection naming a package that is not among `packages` is not dropped:
91/// each of its objects comes back refused, naming the package.
92///
93/// A face that holds its packages in separate owners - the front end has one wasm
94/// `Session` per package - cannot hand them in one call: it calls
95/// [`crosses::traces`], then [`read::contribution_cached`] in each, then
96/// [`assemble::assemble`]. That is this function, cut where its owners are.
97pub fn compute_packages(
98    inputs: &Inputs,
99    packages: &mut [Opened<'_>],
100    reprojector: &dyn energyml_domain::reproject::Reprojector,
101) -> Result<Catalogue, InputsError> {
102    let scene = read::Scene {
103        system: inputs.system.as_ref(),
104        time_factor: inputs.time_factor,
105        reprojector,
106    };
107    compute(inputs, |asked, sections| {
108        match packages.iter_mut().find(|one| one.store == asked.store) {
109            Some(opened) => {
110                let mut thrown = read::PackageCache::new();
111                let cache = opened.cache.as_deref_mut().unwrap_or(&mut thrown);
112                read::contribution_cached(opened.source, asked, sections, &scene, &opened.context, cache)
113            }
114            None => not_open(asked),
115        }
116    })
117}
118
119/// Every object of a selection whose package is not open, refused by name.
120fn not_open(asked: &Asked) -> Contribution {
121    let refused = asked
122        .wells
123        .iter()
124        .chain(&asked.surfaces)
125        .map(|uri| model::Refused {
126            object: model::ObjectId {
127                store: asked.store.clone(),
128                uri: uri.clone(),
129                uuid: read::surfaces::uuid_of_uri(uri),
130                title: String::new(),
131                qualified_type: String::new(),
132            },
133            reason: format!(
134                "the package `{}` this object was asked from is not open",
135                asked.store
136            ),
137        })
138        .collect();
139    Contribution {
140        store: asked.store.clone(),
141        refused,
142        ..Contribution::default()
143    }
144}
145
146/// Why a drawing could not be made again.
147#[derive(Debug, Clone, PartialEq, thiserror::Error)]
148pub enum RedrawError {
149    /// The SVG's metadata does not read.
150    #[error(transparent)]
151    Metadata(#[from] svg::MetadataError),
152    /// What the metadata says cannot be computed.
153    #[error(transparent)]
154    Inputs(#[from] InputsError),
155}
156
157/// **A drawing made again** from what its own metadata says it was made of -
158/// the inputs and the view - over the packages given (`--from-svg`).
159///
160/// On the same packages, the same bytes: that is how a drawing kept beside a
161/// model is known to be the model's still.
162pub fn redraw(
163    svg: &str,
164    packages: &mut [Opened<'_>],
165    reprojector: &dyn energyml_domain::reproject::Reprojector,
166) -> Result<String, RedrawError> {
167    let metadata = svg::read_metadata(svg)?;
168    let catalogue = compute_packages(&metadata.inputs, packages, reprojector)?;
169    Ok(draw(&catalogue, &metadata.view))
170}
171
172/// A catalogue drawn for a view: [`layout::layout`] then [`svg::write`] - what
173/// the screen, the download, the CLI and the package all call (one
174/// drawer, so the export is what was shown).
175pub fn draw(catalogue: &Catalogue, view: &View) -> String {
176    svg::write(&layout::layout(catalogue, view))
177}