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, §ions))
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}