forked from StudioInfinity/dyna3
chore: remove trailing whitespace, add CR at end of file
This commit is contained in:
parent
a4b355d943
commit
3635abc562
11 changed files with 320 additions and 320 deletions
|
@ -45,7 +45,7 @@ static NEXT_SERIAL: AtomicU64 = AtomicU64::new(0);
|
|||
pub trait Serial {
|
||||
// a serial number that uniquely identifies this element
|
||||
fn serial(&self) -> u64;
|
||||
|
||||
|
||||
// take the next serial number, panicking if that was the last one left
|
||||
fn next_serial() -> u64 where Self: Sized {
|
||||
// the technique we use to panic on overflow is taken from _Rust Atomics
|
||||
|
@ -101,33 +101,33 @@ pub trait ProblemPoser {
|
|||
pub trait Element: Serial + ProblemPoser + DisplayItem {
|
||||
// the default identifier for an element of this type
|
||||
fn default_id() -> String where Self: Sized;
|
||||
|
||||
|
||||
// the default example of an element of this type
|
||||
fn default(id: String, id_num: u64) -> Self where Self: Sized;
|
||||
|
||||
|
||||
// the default regulators that come with this element
|
||||
fn default_regulators(self: Rc<Self>) -> Vec<Rc<dyn Regulator>> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
|
||||
fn id(&self) -> &String;
|
||||
fn label(&self) -> &String;
|
||||
fn representation(&self) -> Signal<DVector<f64>>;
|
||||
fn ghost(&self) -> Signal<bool>;
|
||||
|
||||
|
||||
// the regulators the element is subject to. the assembly that owns the
|
||||
// element is responsible for keeping this set up to date
|
||||
fn regulators(&self) -> Signal<BTreeSet<Rc<dyn Regulator>>>;
|
||||
|
||||
|
||||
// project a representation vector for this kind of element onto its
|
||||
// normalization variety
|
||||
fn project_to_normalized(&self, rep: &mut DVector<f64>);
|
||||
|
||||
|
||||
// the configuration matrix column index that was assigned to the element
|
||||
// last time the assembly was realized, or `None` if the element has never
|
||||
// been through a realization
|
||||
fn column_index(&self) -> Option<usize>;
|
||||
|
||||
|
||||
// assign the element a configuration matrix column index. this method must
|
||||
// be used carefully to preserve invariant (1), described in the comment on
|
||||
// the `tangent` field of the `Assembly` structure
|
||||
|
@ -179,7 +179,7 @@ pub struct Sphere {
|
|||
|
||||
impl Sphere {
|
||||
const CURVATURE_COMPONENT: usize = 3;
|
||||
|
||||
|
||||
pub fn new(
|
||||
id: String,
|
||||
label: String,
|
||||
|
@ -203,7 +203,7 @@ impl Element for Sphere {
|
|||
fn default_id() -> String {
|
||||
"sphere".to_string()
|
||||
}
|
||||
|
||||
|
||||
fn default(id: String, id_num: u64) -> Self {
|
||||
Self::new(
|
||||
id,
|
||||
|
@ -212,39 +212,39 @@ impl Element for Sphere {
|
|||
sphere(0.0, 0.0, 0.0, 1.0),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
fn default_regulators(self: Rc<Self>) -> Vec<Rc<dyn Regulator>> {
|
||||
vec![Rc::new(HalfCurvatureRegulator::new(self))]
|
||||
}
|
||||
|
||||
|
||||
fn id(&self) -> &String {
|
||||
&self.id
|
||||
}
|
||||
|
||||
|
||||
fn label(&self) -> &String {
|
||||
&self.label
|
||||
}
|
||||
|
||||
|
||||
fn representation(&self) -> Signal<DVector<f64>> {
|
||||
self.representation
|
||||
}
|
||||
|
||||
|
||||
fn ghost(&self) -> Signal<bool> {
|
||||
self.ghost
|
||||
}
|
||||
|
||||
|
||||
fn regulators(&self) -> Signal<BTreeSet<Rc<dyn Regulator>>> {
|
||||
self.regulators
|
||||
}
|
||||
|
||||
|
||||
fn project_to_normalized(&self, rep: &mut DVector<f64>) {
|
||||
project_sphere_to_normalized(rep);
|
||||
}
|
||||
|
||||
|
||||
fn column_index(&self) -> Option<usize> {
|
||||
self.column_index.get()
|
||||
}
|
||||
|
||||
|
||||
fn set_column_index(&self, index: usize) {
|
||||
self.column_index.set(Some(index));
|
||||
}
|
||||
|
@ -279,7 +279,7 @@ pub struct Point {
|
|||
impl Point {
|
||||
const WEIGHT_COMPONENT: usize = 3;
|
||||
const NORM_COMPONENT: usize = 4;
|
||||
|
||||
|
||||
pub fn new(
|
||||
id: String,
|
||||
label: String,
|
||||
|
@ -303,7 +303,7 @@ impl Element for Point {
|
|||
fn default_id() -> String {
|
||||
"point".to_string()
|
||||
}
|
||||
|
||||
|
||||
fn default(id: String, id_num: u64) -> Self {
|
||||
Self::new(
|
||||
id,
|
||||
|
@ -321,35 +321,35 @@ impl Element for Point {
|
|||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
|
||||
fn id(&self) -> &String {
|
||||
&self.id
|
||||
}
|
||||
|
||||
|
||||
fn label(&self) -> &String {
|
||||
&self.label
|
||||
}
|
||||
|
||||
|
||||
fn representation(&self) -> Signal<DVector<f64>> {
|
||||
self.representation
|
||||
}
|
||||
|
||||
|
||||
fn ghost(&self) -> Signal<bool> {
|
||||
self.ghost
|
||||
}
|
||||
|
||||
|
||||
fn regulators(&self) -> Signal<BTreeSet<Rc<dyn Regulator>>> {
|
||||
self.regulators
|
||||
}
|
||||
|
||||
|
||||
fn project_to_normalized(&self, rep: &mut DVector<f64>) {
|
||||
project_point_to_normalized(rep);
|
||||
}
|
||||
|
||||
|
||||
fn column_index(&self) -> Option<usize> {
|
||||
self.column_index.get()
|
||||
}
|
||||
|
||||
|
||||
fn set_column_index(&self, index: usize) {
|
||||
self.column_index.set(Some(index));
|
||||
}
|
||||
|
@ -420,10 +420,10 @@ impl InversiveDistanceRegulator {
|
|||
)
|
||||
)
|
||||
});
|
||||
|
||||
|
||||
let set_point = create_signal(SpecifiedValue::from_empty_spec());
|
||||
let serial = Self::next_serial();
|
||||
|
||||
|
||||
Self { subjects, measurement, set_point, serial }
|
||||
}
|
||||
}
|
||||
|
@ -432,11 +432,11 @@ impl Regulator for InversiveDistanceRegulator {
|
|||
fn subjects(&self) -> Vec<Rc<dyn Element>> {
|
||||
self.subjects.clone().into()
|
||||
}
|
||||
|
||||
|
||||
fn measurement(&self) -> ReadSignal<f64> {
|
||||
self.measurement
|
||||
}
|
||||
|
||||
|
||||
fn set_point(&self) -> Signal<SpecifiedValue> {
|
||||
self.set_point
|
||||
}
|
||||
|
@ -475,10 +475,10 @@ impl HalfCurvatureRegulator {
|
|||
let measurement = subject.representation().map(
|
||||
|rep| rep[Sphere::CURVATURE_COMPONENT]
|
||||
);
|
||||
|
||||
|
||||
let set_point = create_signal(SpecifiedValue::from_empty_spec());
|
||||
let serial = Self::next_serial();
|
||||
|
||||
|
||||
Self { subject, measurement, set_point, serial }
|
||||
}
|
||||
}
|
||||
|
@ -487,11 +487,11 @@ impl Regulator for HalfCurvatureRegulator {
|
|||
fn subjects(&self) -> Vec<Rc<dyn Element>> {
|
||||
vec![self.subject.clone()]
|
||||
}
|
||||
|
||||
|
||||
fn measurement(&self) -> ReadSignal<f64> {
|
||||
self.measurement
|
||||
}
|
||||
|
||||
|
||||
fn set_point(&self) -> Signal<SpecifiedValue> {
|
||||
self.set_point
|
||||
}
|
||||
|
@ -600,7 +600,7 @@ pub struct Assembly {
|
|||
// elements and regulators
|
||||
pub elements: Signal<BTreeSet<Rc<dyn Element>>>,
|
||||
pub regulators: Signal<BTreeSet<Rc<dyn Regulator>>>,
|
||||
|
||||
|
||||
// solution variety tangent space. the basis vectors are stored in
|
||||
// configuration matrix format, ordered according to the elements' column
|
||||
// indices. when you realize the assembly, every element that's present
|
||||
|
@ -612,13 +612,13 @@ pub struct Assembly {
|
|||
// in that column of the tangent space basis matrices
|
||||
//
|
||||
pub tangent: Signal<ConfigSubspace>,
|
||||
|
||||
|
||||
// indexing
|
||||
pub elements_by_id: Signal<BTreeMap<String, Rc<dyn Element>>>,
|
||||
|
||||
|
||||
// realization control
|
||||
pub realization_trigger: Signal<()>,
|
||||
|
||||
|
||||
// realization diagnostics
|
||||
pub realization_status: Signal<Result<(), String>>,
|
||||
pub descent_history: Signal<DescentHistory>,
|
||||
|
@ -638,7 +638,7 @@ impl Assembly {
|
|||
descent_history: create_signal(DescentHistory::new()),
|
||||
step: create_signal(SpecifiedValue::from_empty_spec()),
|
||||
};
|
||||
|
||||
|
||||
// realize the assembly whenever the element list, the regulator list,
|
||||
// a regulator's set point, or the realization trigger is updated
|
||||
let assembly_for_realization = assembly.clone();
|
||||
|
@ -652,7 +652,7 @@ impl Assembly {
|
|||
assembly_for_realization.realization_trigger.track();
|
||||
assembly_for_realization.realize();
|
||||
});
|
||||
|
||||
|
||||
// load a configuration from the descent history whenever the active
|
||||
// step is updated
|
||||
let assembly_for_step_selection = assembly.clone();
|
||||
|
@ -664,12 +664,12 @@ impl Assembly {
|
|||
assembly_for_step_selection.load_config(&config)
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
assembly
|
||||
}
|
||||
|
||||
|
||||
// --- inserting elements and regulators ---
|
||||
|
||||
|
||||
// insert an element into the assembly without checking whether we already
|
||||
// have an element with the same identifier. any element that does have the
|
||||
// same identifier will get kicked out of the `elements_by_id` index
|
||||
|
@ -679,13 +679,13 @@ impl Assembly {
|
|||
let elt_rc = Rc::new(elt);
|
||||
self.elements.update(|elts| elts.insert(elt_rc.clone()));
|
||||
self.elements_by_id.update(|elts_by_id| elts_by_id.insert(id, elt_rc.clone()));
|
||||
|
||||
|
||||
// create and insert the element's default regulators
|
||||
for reg in elt_rc.default_regulators() {
|
||||
self.insert_regulator(reg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn try_insert_element(&self, elt: impl Element + 'static) -> bool {
|
||||
let can_insert = self.elements_by_id.with_untracked(
|
||||
|elts_by_id| !elts_by_id.contains_key(elt.id())
|
||||
|
@ -695,7 +695,7 @@ impl Assembly {
|
|||
}
|
||||
can_insert
|
||||
}
|
||||
|
||||
|
||||
pub fn insert_element_default<T: Element + 'static>(&self) {
|
||||
// find the next unused identifier in the default sequence
|
||||
let default_id = T::default_id();
|
||||
|
@ -707,17 +707,17 @@ impl Assembly {
|
|||
id_num += 1;
|
||||
id = format!("{default_id}{id_num}");
|
||||
}
|
||||
|
||||
|
||||
// create and insert the default example of `T`
|
||||
let _ = self.insert_element_unchecked(T::default(id, id_num));
|
||||
}
|
||||
|
||||
|
||||
pub fn insert_regulator(&self, regulator: Rc<dyn Regulator>) {
|
||||
// add the regulator to the assembly's regulator list
|
||||
self.regulators.update(
|
||||
|regs| regs.insert(regulator.clone())
|
||||
);
|
||||
|
||||
|
||||
// add the regulator to each subject's regulator list
|
||||
let subject_regulators: Vec<_> = regulator.subjects().into_iter().map(
|
||||
|subj| subj.regulators()
|
||||
|
@ -725,7 +725,7 @@ impl Assembly {
|
|||
for regulators in subject_regulators {
|
||||
regulators.update(|regs| regs.insert(regulator.clone()));
|
||||
}
|
||||
|
||||
|
||||
/* DEBUG */
|
||||
// print an updated list of regulators
|
||||
console_log!("Regulators:");
|
||||
|
@ -748,9 +748,9 @@ impl Assembly {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// --- updating the configuration ---
|
||||
|
||||
|
||||
pub fn load_config(&self, config: &DMatrix<f64>) {
|
||||
for elt in self.elements.get_clone_untracked() {
|
||||
elt.representation().update(
|
||||
|
@ -758,9 +758,9 @@ impl Assembly {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- realization ---
|
||||
|
||||
|
||||
pub fn realize(&self) {
|
||||
// index the elements
|
||||
self.elements.update_silent(|elts| {
|
||||
|
@ -768,7 +768,7 @@ impl Assembly {
|
|||
elt.set_column_index(index);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// set up the constraint problem
|
||||
let problem = self.elements.with_untracked(|elts| {
|
||||
let mut problem = ConstraintProblem::new(elts.len());
|
||||
|
@ -782,21 +782,21 @@ impl Assembly {
|
|||
});
|
||||
problem
|
||||
});
|
||||
|
||||
|
||||
/* DEBUG */
|
||||
// log the Gram matrix
|
||||
console_log!("Gram matrix:\n{}", problem.gram);
|
||||
console_log!("Frozen entries:\n{}", problem.frozen);
|
||||
|
||||
|
||||
/* DEBUG */
|
||||
// log the initial configuration matrix
|
||||
console_log!("Old configuration:{:>8.3}", problem.guess);
|
||||
|
||||
|
||||
// look for a configuration with the given Gram matrix
|
||||
let Realization { result, history } = realize_gram(
|
||||
&problem, 1.0e-12, 0.5, 0.9, 1.1, 200, 110
|
||||
);
|
||||
|
||||
|
||||
/* DEBUG */
|
||||
// report the outcome of the search in the browser console
|
||||
if let Err(ref message) = result {
|
||||
|
@ -808,20 +808,20 @@ impl Assembly {
|
|||
console_log!("Steps: {}", history.scaled_loss.len() - 1);
|
||||
console_log!("Loss: {}", history.scaled_loss.last().unwrap());
|
||||
}
|
||||
|
||||
|
||||
// report the descent history
|
||||
let step_cnt = history.config.len();
|
||||
self.descent_history.set(history);
|
||||
|
||||
|
||||
match result {
|
||||
Ok(ConfigNeighborhood { nbhd: tangent, .. }) => {
|
||||
/* DEBUG */
|
||||
// report the tangent dimension
|
||||
console_log!("Tangent dimension: {}", tangent.dim());
|
||||
|
||||
|
||||
// report the realization status
|
||||
self.realization_status.set(Ok(()));
|
||||
|
||||
|
||||
// display the last realization step
|
||||
self.step.set(
|
||||
if step_cnt > 0 {
|
||||
|
@ -831,7 +831,7 @@ impl Assembly {
|
|||
SpecifiedValue::from_empty_spec()
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
// save the tangent space
|
||||
self.tangent.set_silent(tangent);
|
||||
},
|
||||
|
@ -841,15 +841,15 @@ impl Assembly {
|
|||
// `Err(message)` we received from the match: we're changing the
|
||||
// `Ok` type from `Realization` to `()`
|
||||
self.realization_status.set(Err(message));
|
||||
|
||||
|
||||
// display the initial guess
|
||||
self.step.set(SpecifiedValue::from(Some(0.0)));
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- deformation ---
|
||||
|
||||
|
||||
// project the given motion to the tangent space of the solution variety and
|
||||
// move the assembly along it. the implementation is based on invariant (1)
|
||||
// from above and the following additional invariant:
|
||||
|
@ -866,7 +866,7 @@ impl Assembly {
|
|||
if self.tangent.with(|tan| tan.dim() <= 0 && tan.assembly_dim() > 0) {
|
||||
console::log_1(&JsValue::from("The assembly is rigid"));
|
||||
}
|
||||
|
||||
|
||||
// give a column index to each moving element that doesn't have one yet.
|
||||
// this temporarily breaks invariant (1), but the invariant will be
|
||||
// restored when we realize the assembly at the end of the deformation.
|
||||
|
@ -884,7 +884,7 @@ impl Assembly {
|
|||
}
|
||||
next_column_index
|
||||
};
|
||||
|
||||
|
||||
// project the element motions onto the tangent space of the solution
|
||||
// variety and sum them to get a deformation of the whole assembly. the
|
||||
// matrix `motion_proj` that holds the deformation has extra columns for
|
||||
|
@ -895,7 +895,7 @@ impl Assembly {
|
|||
// we can unwrap the column index because we know that every moving
|
||||
// element has one at this point
|
||||
let column_index = elt_motion.element.column_index().unwrap();
|
||||
|
||||
|
||||
if column_index < realized_dim {
|
||||
// this element had a column index when we started, so by
|
||||
// invariant (1), it's reflected in the tangent space
|
||||
|
@ -913,7 +913,7 @@ impl Assembly {
|
|||
target_column += unif_to_std * elt_motion.velocity;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// step the assembly along the deformation. this changes the elements'
|
||||
// normalizations, so we restore those afterward
|
||||
for elt in self.elements.get_clone_untracked() {
|
||||
|
@ -931,7 +931,7 @@ impl Assembly {
|
|||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// trigger a realization to bring the configuration back onto the
|
||||
// solution variety. this also gets the elements' column indices and the
|
||||
// saved tangent space back in sync
|
||||
|
@ -942,9 +942,9 @@ impl Assembly {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
|
||||
use crate::engine;
|
||||
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected =
|
||||
"Sphere \"sphere\" must be indexed before it writes problem data")]
|
||||
|
@ -954,7 +954,7 @@ mod tests {
|
|||
elt.pose(&mut ConstraintProblem::new(1));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Subject \"sphere1\" must be indexed before \
|
||||
inversive distance regulator writes problem data")]
|
||||
|
@ -972,7 +972,7 @@ mod tests {
|
|||
}.pose(&mut ConstraintProblem::new(2));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn curvature_drift_test() {
|
||||
const INITIAL_RADIUS: f64 = 0.25;
|
||||
|
@ -992,7 +992,7 @@ mod tests {
|
|||
engine::sphere(0.0, 0.0, 0.0, INITIAL_RADIUS),
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
// nudge the sphere repeatedly along the `z` axis
|
||||
const STEP_SIZE: f64 = 0.0025;
|
||||
const STEP_CNT: usize = 400;
|
||||
|
@ -1008,7 +1008,7 @@ mod tests {
|
|||
]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// check how much the sphere's curvature has drifted
|
||||
const INITIAL_HALF_CURV: f64 = 0.5 / INITIAL_RADIUS;
|
||||
const DRIFT_TOL: f64 = 0.015;
|
||||
|
|
|
@ -54,7 +54,7 @@ fn StepInput() -> View {
|
|||
// get the assembly
|
||||
let state = use_context::<AppState>();
|
||||
let assembly = state.assembly;
|
||||
|
||||
|
||||
// the `last_step` signal holds the index of the last step
|
||||
let last_step = assembly.descent_history.map(
|
||||
|history| match history.config.len() {
|
||||
|
@ -63,15 +63,15 @@ fn StepInput() -> View {
|
|||
}
|
||||
);
|
||||
let input_max = last_step.map(|last| last.unwrap_or(0));
|
||||
|
||||
|
||||
// these signals hold the entered step number
|
||||
let value = create_signal(String::new());
|
||||
let value_as_number = create_signal(0.0);
|
||||
|
||||
|
||||
create_effect(move || {
|
||||
value.set(assembly.step.with(|n| n.spec.clone()));
|
||||
});
|
||||
|
||||
|
||||
view! {
|
||||
div(id = "step-input") {
|
||||
label { "Step" }
|
||||
|
@ -98,7 +98,7 @@ fn StepInput() -> View {
|
|||
|val| val.clamp(0.0, input_max.get() as f64)
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
// set the input string and the assembly's active step
|
||||
value.set(step.spec.clone());
|
||||
assembly.step.set(step);
|
||||
|
@ -124,7 +124,7 @@ fn LossHistory() -> View {
|
|||
const CONTAINER_ID: &str = "loss-history";
|
||||
let state = use_context::<AppState>();
|
||||
let renderer = WasmRenderer::new_opt(None, Some(178));
|
||||
|
||||
|
||||
on_mount(move || {
|
||||
create_effect(move || {
|
||||
// get the loss history
|
||||
|
@ -136,13 +136,13 @@ fn LossHistory() -> View {
|
|||
.map(into_log10_time_point)
|
||||
.collect()
|
||||
);
|
||||
|
||||
|
||||
// initialize the chart axes
|
||||
let step_axis = Axis::new()
|
||||
.type_(AxisType::Category)
|
||||
.boundary_gap(false);
|
||||
let scaled_loss_axis = Axis::new();
|
||||
|
||||
|
||||
// load the chart data. when there's no history, we load the data
|
||||
// point (0, None) to clear the chart. it would feel more natural to
|
||||
// load empty data vectors, but that turns out not to clear the
|
||||
|
@ -164,7 +164,7 @@ fn LossHistory() -> View {
|
|||
renderer.render(CONTAINER_ID, &chart).unwrap();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
view! {
|
||||
div(id = CONTAINER_ID, class = "diagnostics-chart")
|
||||
}
|
||||
|
@ -176,7 +176,7 @@ fn SpectrumHistory() -> View {
|
|||
const CONTAINER_ID: &str = "spectrum-history";
|
||||
let state = use_context::<AppState>();
|
||||
let renderer = WasmRenderer::new(478, 178);
|
||||
|
||||
|
||||
on_mount(move || {
|
||||
create_effect(move || {
|
||||
// get the spectrum of the Hessian at each step, split into its
|
||||
|
@ -208,13 +208,13 @@ fn SpectrumHistory() -> View {
|
|||
): (Vec<_>, Vec<_>) = hess_eigvals_nonzero
|
||||
.into_iter()
|
||||
.partition(|&(_, val)| val > 0.0);
|
||||
|
||||
|
||||
// initialize the chart axes
|
||||
let step_axis = Axis::new()
|
||||
.type_(AxisType::Category)
|
||||
.boundary_gap(false);
|
||||
let eigval_axis = Axis::new();
|
||||
|
||||
|
||||
// load the chart data. when there's no history, we load the data
|
||||
// point (0, None) to clear the chart. it would feel more natural to
|
||||
// load empty data vectors, but that turns out not to clear the
|
||||
|
@ -270,7 +270,7 @@ fn SpectrumHistory() -> View {
|
|||
renderer.render(CONTAINER_ID, &chart).unwrap();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
view! {
|
||||
div(id = CONTAINER_ID, class = "diagnostics-chart")
|
||||
}
|
||||
|
@ -302,7 +302,7 @@ pub fn Diagnostics() -> View {
|
|||
let diagnostics_state = DiagnosticsState::new("loss".to_string());
|
||||
let active_tab = diagnostics_state.active_tab.clone();
|
||||
provide_context(diagnostics_state);
|
||||
|
||||
|
||||
view! {
|
||||
div(id = "diagnostics") {
|
||||
div(id = "diagnostics-bar") {
|
||||
|
@ -317,4 +317,4 @@ pub fn Diagnostics() -> View {
|
|||
DiagnosticsPanel(name = "spectrum") { SpectrumHistory {} }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,11 +48,11 @@ impl SceneSpheres {
|
|||
highlights: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn len_i32(&self) -> i32 {
|
||||
self.representations.len().try_into().expect("Number of spheres must fit in a 32-bit integer")
|
||||
}
|
||||
|
||||
|
||||
fn push(
|
||||
&mut self, representation: DVector<f64>,
|
||||
color: ElementColor, opacity: f32, highlight: f32,
|
||||
|
@ -79,7 +79,7 @@ impl ScenePoints {
|
|||
selections: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn push(
|
||||
&mut self, representation: DVector<f64>,
|
||||
color: ElementColor, opacity: f32, highlight: f32, selected: bool,
|
||||
|
@ -107,7 +107,7 @@ impl Scene {
|
|||
|
||||
pub trait DisplayItem {
|
||||
fn show(&self, scene: &mut Scene, selected: bool);
|
||||
|
||||
|
||||
// the smallest positive depth, represented as a multiple of `dir`, where
|
||||
// the line generated by `dir` hits the element. returns `None` if the line
|
||||
// misses the element
|
||||
|
@ -125,14 +125,14 @@ impl DisplayItem for Sphere {
|
|||
const DEFAULT_OPACITY: f32 = 0.5;
|
||||
const GHOST_OPACITY: f32 = 0.2;
|
||||
const HIGHLIGHT: f32 = 0.2;
|
||||
|
||||
|
||||
let representation = self.representation.get_clone_untracked();
|
||||
let color = if selected { self.color.map(|channel| 0.2 + 0.8*channel) } else { self.color };
|
||||
let opacity = if self.ghost.get() { GHOST_OPACITY } else { DEFAULT_OPACITY };
|
||||
let highlight = if selected { 1.0 } else { HIGHLIGHT };
|
||||
scene.spheres.push(representation, color, opacity, highlight);
|
||||
}
|
||||
|
||||
|
||||
// this method should be kept synchronized with `sphere_cast` in
|
||||
// `spheres.frag`, which does essentially the same thing on the GPU side
|
||||
fn cast(
|
||||
|
@ -144,12 +144,12 @@ impl DisplayItem for Sphere {
|
|||
// if `a/b` is less than this threshold, we approximate
|
||||
// `a*u^2 + b*u + c` by the linear function `b*u + c`
|
||||
const DEG_THRESHOLD: f64 = 1e-9;
|
||||
|
||||
|
||||
let rep = self.representation.with_untracked(|rep| assembly_to_world * rep);
|
||||
let a = -rep[3] * dir.norm_squared();
|
||||
let b = rep.rows_range(..3).dot(&dir);
|
||||
let c = -rep[4];
|
||||
|
||||
|
||||
let adjust = 4.0*a*c/(b*b);
|
||||
if adjust < 1.0 {
|
||||
// as long as `b` is non-zero, the linear approximation of
|
||||
|
@ -184,14 +184,14 @@ impl DisplayItem for Point {
|
|||
/* SCAFFOLDING */
|
||||
const GHOST_OPACITY: f32 = 0.4;
|
||||
const HIGHLIGHT: f32 = 0.5;
|
||||
|
||||
|
||||
let representation = self.representation.get_clone_untracked();
|
||||
let color = if selected { self.color.map(|channel| 0.2 + 0.8*channel) } else { self.color };
|
||||
let opacity = if self.ghost.get() { GHOST_OPACITY } else { 1.0 };
|
||||
let highlight = if selected { 1.0 } else { HIGHLIGHT };
|
||||
scene.points.push(representation, color, opacity, highlight, selected);
|
||||
}
|
||||
|
||||
|
||||
/* SCAFFOLDING */
|
||||
fn cast(
|
||||
&self,
|
||||
|
@ -203,16 +203,16 @@ impl DisplayItem for Point {
|
|||
if rep[2] < 0.0 {
|
||||
// this constant should be kept synchronized with `point.frag`
|
||||
const POINT_RADIUS_PX: f64 = 4.0;
|
||||
|
||||
|
||||
// find the radius of the point in screen projection units
|
||||
let point_radius_proj = POINT_RADIUS_PX * pixel_size;
|
||||
|
||||
|
||||
// find the squared distance between the screen projections of the
|
||||
// ray and the point
|
||||
let dir_proj = -dir.fixed_rows::<2>(0) / dir[2];
|
||||
let rep_proj = -rep.fixed_rows::<2>(0) / rep[2];
|
||||
let dist_sq = (dir_proj - rep_proj).norm_squared();
|
||||
|
||||
|
||||
// if the ray hits the point, return its depth
|
||||
if dist_sq < point_radius_proj * point_radius_proj {
|
||||
Some(rep[2] / dir[2])
|
||||
|
@ -254,13 +254,13 @@ fn set_up_program(
|
|||
WebGl2RenderingContext::FRAGMENT_SHADER,
|
||||
fragment_shader_source,
|
||||
);
|
||||
|
||||
|
||||
// create the program and attach the shaders
|
||||
let program = context.create_program().unwrap();
|
||||
context.attach_shader(&program, &vertex_shader);
|
||||
context.attach_shader(&program, &fragment_shader);
|
||||
context.link_program(&program);
|
||||
|
||||
|
||||
/* DEBUG */
|
||||
// report whether linking succeeded
|
||||
let link_status = context
|
||||
|
@ -273,7 +273,7 @@ fn set_up_program(
|
|||
"Linking failed"
|
||||
};
|
||||
console::log_1(&JsValue::from(link_msg));
|
||||
|
||||
|
||||
program
|
||||
}
|
||||
|
||||
|
@ -318,7 +318,7 @@ fn load_new_buffer(
|
|||
// create a buffer and bind it to ARRAY_BUFFER
|
||||
let buffer = context.create_buffer();
|
||||
context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, buffer.as_ref());
|
||||
|
||||
|
||||
// load the given data into the buffer. this block is unsafe because
|
||||
// `Float32Array::view` creates a raw view into our module's
|
||||
// `WebAssembly.Memory` buffer. allocating more memory will change the
|
||||
|
@ -332,7 +332,7 @@ fn load_new_buffer(
|
|||
WebGl2RenderingContext::STATIC_DRAW,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
buffer
|
||||
}
|
||||
|
||||
|
@ -353,11 +353,11 @@ fn event_dir(event: &MouseEvent) -> (Vector3<f64>, f64) {
|
|||
let width = rect.width();
|
||||
let height = rect.height();
|
||||
let shortdim = width.min(height);
|
||||
|
||||
|
||||
// this constant should be kept synchronized with `spheres.frag` and
|
||||
// `point.vert`
|
||||
const FOCAL_SLOPE: f64 = 0.3;
|
||||
|
||||
|
||||
(
|
||||
Vector3::new(
|
||||
FOCAL_SLOPE * (2.0*(f64::from(event.client_x()) - rect.left()) - width) / shortdim,
|
||||
|
@ -373,13 +373,13 @@ fn event_dir(event: &MouseEvent) -> (Vector3<f64>, f64) {
|
|||
#[component]
|
||||
pub fn Display() -> View {
|
||||
let state = use_context::<AppState>();
|
||||
|
||||
|
||||
// canvas
|
||||
let display = create_node_ref();
|
||||
|
||||
|
||||
// viewpoint
|
||||
let assembly_to_world = create_signal(DMatrix::<f64>::identity(5, 5));
|
||||
|
||||
|
||||
// navigation
|
||||
let pitch_up = create_signal(0.0);
|
||||
let pitch_down = create_signal(0.0);
|
||||
|
@ -390,7 +390,7 @@ pub fn Display() -> View {
|
|||
let zoom_in = create_signal(0.0);
|
||||
let zoom_out = create_signal(0.0);
|
||||
let turntable = create_signal(false); /* BENCHMARKING */
|
||||
|
||||
|
||||
// manipulation
|
||||
let translate_neg_x = create_signal(0.0);
|
||||
let translate_pos_x = create_signal(0.0);
|
||||
|
@ -400,7 +400,7 @@ pub fn Display() -> View {
|
|||
let translate_pos_z = create_signal(0.0);
|
||||
let shrink_neg = create_signal(0.0);
|
||||
let shrink_pos = create_signal(0.0);
|
||||
|
||||
|
||||
// change listener
|
||||
let scene_changed = create_signal(true);
|
||||
create_effect(move || {
|
||||
|
@ -413,18 +413,18 @@ pub fn Display() -> View {
|
|||
state.selection.track();
|
||||
scene_changed.set(true);
|
||||
});
|
||||
|
||||
|
||||
/* INSTRUMENTS */
|
||||
const SAMPLE_PERIOD: i32 = 60;
|
||||
let mut last_sample_time = 0.0;
|
||||
let mut frames_since_last_sample = 0;
|
||||
let mean_frame_interval = create_signal(0.0);
|
||||
|
||||
|
||||
let assembly_for_raf = state.assembly.clone();
|
||||
on_mount(move || {
|
||||
// timing
|
||||
let mut last_time = 0.0;
|
||||
|
||||
|
||||
// viewpoint
|
||||
const ROT_SPEED: f64 = 0.4; // in radians per second
|
||||
const ZOOM_SPEED: f64 = 0.15; // multiplicative rate per second
|
||||
|
@ -432,18 +432,18 @@ pub fn Display() -> View {
|
|||
let mut orientation = DMatrix::<f64>::identity(5, 5);
|
||||
let mut rotation = DMatrix::<f64>::identity(5, 5);
|
||||
let mut location_z: f64 = 5.0;
|
||||
|
||||
|
||||
// manipulation
|
||||
const TRANSLATION_SPEED: f64 = 0.15; // in length units per second
|
||||
const SHRINKING_SPEED: f64 = 0.15; // in length units per second
|
||||
|
||||
|
||||
// display parameters
|
||||
const LAYER_THRESHOLD: i32 = 0; /* DEBUG */
|
||||
const DEBUG_MODE: i32 = 0; /* DEBUG */
|
||||
|
||||
|
||||
/* INSTRUMENTS */
|
||||
let performance = window().unwrap().performance().unwrap();
|
||||
|
||||
|
||||
// get the display canvas
|
||||
let canvas = display.get().unchecked_into::<web_sys::HtmlCanvasElement>();
|
||||
let ctx = canvas
|
||||
|
@ -452,28 +452,28 @@ pub fn Display() -> View {
|
|||
.unwrap()
|
||||
.dyn_into::<WebGl2RenderingContext>()
|
||||
.unwrap();
|
||||
|
||||
|
||||
// disable depth testing
|
||||
ctx.disable(WebGl2RenderingContext::DEPTH_TEST);
|
||||
|
||||
|
||||
// set blend mode
|
||||
ctx.enable(WebGl2RenderingContext::BLEND);
|
||||
ctx.blend_func(WebGl2RenderingContext::SRC_ALPHA, WebGl2RenderingContext::ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
|
||||
// set up the sphere rendering program
|
||||
let sphere_program = set_up_program(
|
||||
&ctx,
|
||||
include_str!("identity.vert"),
|
||||
include_str!("spheres.frag"),
|
||||
);
|
||||
|
||||
|
||||
// set up the point rendering program
|
||||
let point_program = set_up_program(
|
||||
&ctx,
|
||||
include_str!("point.vert"),
|
||||
include_str!("point.frag"),
|
||||
);
|
||||
|
||||
|
||||
/* DEBUG */
|
||||
// print the maximum number of vectors that can be passed as
|
||||
// uniforms to a fragment shader. the OpenGL ES 3.0 standard
|
||||
|
@ -490,10 +490,10 @@ pub fn Display() -> View {
|
|||
&ctx.get_parameter(WebGl2RenderingContext::MAX_FRAGMENT_UNIFORM_VECTORS).unwrap(),
|
||||
&JsValue::from("uniform vectors available"),
|
||||
);
|
||||
|
||||
|
||||
// find the sphere program's vertex attribute
|
||||
let viewport_position_attr = ctx.get_attrib_location(&sphere_program, "position") as u32;
|
||||
|
||||
|
||||
// find the sphere program's uniforms
|
||||
const SPHERE_MAX: usize = 200;
|
||||
let sphere_cnt_loc = ctx.get_uniform_location(&sphere_program, "sphere_cnt");
|
||||
|
@ -513,7 +513,7 @@ pub fn Display() -> View {
|
|||
let shortdim_loc = ctx.get_uniform_location(&sphere_program, "shortdim");
|
||||
let layer_threshold_loc = ctx.get_uniform_location(&sphere_program, "layer_threshold");
|
||||
let debug_mode_loc = ctx.get_uniform_location(&sphere_program, "debug_mode");
|
||||
|
||||
|
||||
// load the viewport vertex positions into a new vertex buffer object
|
||||
const VERTEX_CNT: usize = 6;
|
||||
let viewport_positions: [f32; 3*VERTEX_CNT] = [
|
||||
|
@ -527,20 +527,20 @@ pub fn Display() -> View {
|
|||
1.0, -1.0, 0.0,
|
||||
];
|
||||
let viewport_position_buffer = load_new_buffer(&ctx, &viewport_positions);
|
||||
|
||||
|
||||
// find the point program's vertex attributes
|
||||
let point_position_attr = ctx.get_attrib_location(&point_program, "position") as u32;
|
||||
let point_color_attr = ctx.get_attrib_location(&point_program, "color") as u32;
|
||||
let point_highlight_attr = ctx.get_attrib_location(&point_program, "highlight") as u32;
|
||||
let point_selection_attr = ctx.get_attrib_location(&point_program, "selected") as u32;
|
||||
|
||||
|
||||
// set up a repainting routine
|
||||
let (_, start_animation_loop, _) = create_raf(move || {
|
||||
// get the time step
|
||||
let time = performance.now();
|
||||
let time_step = 0.001*(time - last_time);
|
||||
last_time = time;
|
||||
|
||||
|
||||
// get the navigation state
|
||||
let pitch_up_val = pitch_up.get();
|
||||
let pitch_down_val = pitch_down.get();
|
||||
|
@ -551,7 +551,7 @@ pub fn Display() -> View {
|
|||
let zoom_in_val = zoom_in.get();
|
||||
let zoom_out_val = zoom_out.get();
|
||||
let turntable_val = turntable.get(); /* BENCHMARKING */
|
||||
|
||||
|
||||
// get the manipulation state
|
||||
let translate_neg_x_val = translate_neg_x.get();
|
||||
let translate_pos_x_val = translate_pos_x.get();
|
||||
|
@ -561,7 +561,7 @@ pub fn Display() -> View {
|
|||
let translate_pos_z_val = translate_pos_z.get();
|
||||
let shrink_neg_val = shrink_neg.get();
|
||||
let shrink_pos_val = shrink_pos.get();
|
||||
|
||||
|
||||
// update the assembly's orientation
|
||||
let ang_vel = {
|
||||
let pitch = pitch_up_val - pitch_down_val;
|
||||
|
@ -582,11 +582,11 @@ pub fn Display() -> View {
|
|||
Rotation3::from_scaled_axis(time_step * ang_vel).matrix()
|
||||
);
|
||||
orientation = &rotation * &orientation;
|
||||
|
||||
|
||||
// update the assembly's location
|
||||
let zoom = zoom_out_val - zoom_in_val;
|
||||
location_z *= (time_step * ZOOM_SPEED * zoom).exp();
|
||||
|
||||
|
||||
// manipulate the assembly
|
||||
/* KLUDGE */
|
||||
// to avoid the complexity of making tangent space projection
|
||||
|
@ -642,11 +642,11 @@ pub fn Display() -> View {
|
|||
scene_changed.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if scene_changed.get() {
|
||||
const SPACE_DIM: usize = 3;
|
||||
const COLOR_SIZE: usize = 3;
|
||||
|
||||
|
||||
/* INSTRUMENTS */
|
||||
// measure mean frame interval
|
||||
frames_since_last_sample += 1;
|
||||
|
@ -655,11 +655,11 @@ pub fn Display() -> View {
|
|||
last_sample_time = time;
|
||||
frames_since_last_sample = 0;
|
||||
}
|
||||
|
||||
|
||||
// --- get the assembly ---
|
||||
|
||||
|
||||
let mut scene = Scene::new();
|
||||
|
||||
|
||||
// find the map from assembly space to world space
|
||||
let location = {
|
||||
let u = -location_z;
|
||||
|
@ -672,7 +672,7 @@ pub fn Display() -> View {
|
|||
])
|
||||
};
|
||||
let asm_to_world = &location * &orientation;
|
||||
|
||||
|
||||
// set up the scene
|
||||
state.assembly.elements.with_untracked(
|
||||
|elts| for elt in elts {
|
||||
|
@ -681,26 +681,26 @@ pub fn Display() -> View {
|
|||
}
|
||||
);
|
||||
let sphere_cnt = scene.spheres.len_i32();
|
||||
|
||||
|
||||
// --- draw the spheres ---
|
||||
|
||||
|
||||
// use the sphere rendering program
|
||||
ctx.use_program(Some(&sphere_program));
|
||||
|
||||
|
||||
// enable the sphere program's vertex attribute
|
||||
ctx.enable_vertex_attrib_array(viewport_position_attr);
|
||||
|
||||
|
||||
// write the spheres in world coordinates
|
||||
let sphere_reps_world: Vec<_> = scene.spheres.representations.into_iter().map(
|
||||
|rep| (&asm_to_world * rep).cast::<f32>()
|
||||
).collect();
|
||||
|
||||
|
||||
// set the resolution
|
||||
let width = canvas.width() as f32;
|
||||
let height = canvas.height() as f32;
|
||||
ctx.uniform2f(resolution_loc.as_ref(), width, height);
|
||||
ctx.uniform1f(shortdim_loc.as_ref(), width.min(height));
|
||||
|
||||
|
||||
// pass the scene data
|
||||
ctx.uniform1i(sphere_cnt_loc.as_ref(), sphere_cnt);
|
||||
for n in 0..sphere_reps_world.len() {
|
||||
|
@ -722,33 +722,33 @@ pub fn Display() -> View {
|
|||
scene.spheres.highlights[n],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// pass the display parameters
|
||||
ctx.uniform1i(layer_threshold_loc.as_ref(), LAYER_THRESHOLD);
|
||||
ctx.uniform1i(debug_mode_loc.as_ref(), DEBUG_MODE);
|
||||
|
||||
|
||||
// bind the viewport vertex position buffer to the position
|
||||
// attribute in the vertex shader
|
||||
bind_to_attribute(&ctx, viewport_position_attr, SPACE_DIM as i32, &viewport_position_buffer);
|
||||
|
||||
|
||||
// draw the scene
|
||||
ctx.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, VERTEX_CNT as i32);
|
||||
|
||||
|
||||
// disable the sphere program's vertex attribute
|
||||
ctx.disable_vertex_attrib_array(viewport_position_attr);
|
||||
|
||||
|
||||
// --- draw the points ---
|
||||
|
||||
|
||||
if !scene.points.representations.is_empty() {
|
||||
// use the point rendering program
|
||||
ctx.use_program(Some(&point_program));
|
||||
|
||||
|
||||
// enable the point program's vertex attributes
|
||||
ctx.enable_vertex_attrib_array(point_position_attr);
|
||||
ctx.enable_vertex_attrib_array(point_color_attr);
|
||||
ctx.enable_vertex_attrib_array(point_highlight_attr);
|
||||
ctx.enable_vertex_attrib_array(point_selection_attr);
|
||||
|
||||
|
||||
// write the points in world coordinates
|
||||
let asm_to_world_sp = asm_to_world.rows(0, SPACE_DIM);
|
||||
let point_positions = DMatrix::from_columns(
|
||||
|
@ -756,7 +756,7 @@ pub fn Display() -> View {
|
|||
|rep| &asm_to_world_sp * rep
|
||||
).collect::<Vec<_>>().as_slice()
|
||||
).cast::<f32>();
|
||||
|
||||
|
||||
// load the point positions and colors into new buffers and
|
||||
// bind them to the corresponding attributes in the vertex
|
||||
// shader
|
||||
|
@ -764,22 +764,22 @@ pub fn Display() -> View {
|
|||
bind_new_buffer_to_attribute(&ctx, point_color_attr, (COLOR_SIZE + 1) as i32, scene.points.colors_with_opacity.concat().as_slice());
|
||||
bind_new_buffer_to_attribute(&ctx, point_highlight_attr, 1 as i32, scene.points.highlights.as_slice());
|
||||
bind_new_buffer_to_attribute(&ctx, point_selection_attr, 1 as i32, scene.points.selections.as_slice());
|
||||
|
||||
|
||||
// draw the scene
|
||||
ctx.draw_arrays(WebGl2RenderingContext::POINTS, 0, point_positions.ncols() as i32);
|
||||
|
||||
|
||||
// disable the point program's vertex attributes
|
||||
ctx.disable_vertex_attrib_array(point_position_attr);
|
||||
ctx.disable_vertex_attrib_array(point_color_attr);
|
||||
ctx.disable_vertex_attrib_array(point_highlight_attr);
|
||||
ctx.disable_vertex_attrib_array(point_selection_attr);
|
||||
}
|
||||
|
||||
|
||||
// --- update the display state ---
|
||||
|
||||
|
||||
// update the viewpoint
|
||||
assembly_to_world.set(asm_to_world);
|
||||
|
||||
|
||||
// clear the scene change flag
|
||||
scene_changed.set(
|
||||
pitch_up_val != 0.0
|
||||
|
@ -799,7 +799,7 @@ pub fn Display() -> View {
|
|||
});
|
||||
start_animation_loop();
|
||||
});
|
||||
|
||||
|
||||
let set_nav_signal = move |event: &KeyboardEvent, value: f64| {
|
||||
let mut navigating = true;
|
||||
let shift = event.shift_key();
|
||||
|
@ -819,7 +819,7 @@ pub fn Display() -> View {
|
|||
event.prevent_default();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
let set_manip_signal = move |event: &KeyboardEvent, value: f64| {
|
||||
let mut manipulating = true;
|
||||
let shift = event.shift_key();
|
||||
|
@ -838,7 +838,7 @@ pub fn Display() -> View {
|
|||
event.prevent_default();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
view! {
|
||||
/* TO DO */
|
||||
// switch back to integer-valued parameters when that becomes possible
|
||||
|
@ -860,7 +860,7 @@ pub fn Display() -> View {
|
|||
yaw_left.set(0.0);
|
||||
pitch_up.set(0.0);
|
||||
pitch_down.set(0.0);
|
||||
|
||||
|
||||
// swap manipulation inputs
|
||||
translate_pos_z.set(translate_neg_y.get());
|
||||
translate_neg_z.set(translate_pos_y.get());
|
||||
|
@ -886,7 +886,7 @@ pub fn Display() -> View {
|
|||
roll_ccw.set(0.0);
|
||||
zoom_in.set(0.0);
|
||||
zoom_out.set(0.0);
|
||||
|
||||
|
||||
// swap manipulation inputs
|
||||
translate_pos_y.set(translate_neg_z.get());
|
||||
translate_neg_y.set(translate_pos_z.get());
|
||||
|
@ -927,7 +927,7 @@ pub fn Display() -> View {
|
|||
None => (),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// if we clicked something, select it
|
||||
match clicked {
|
||||
Some((elt, _)) => state.select(&elt, event.shift_key()),
|
||||
|
@ -936,4 +936,4 @@ pub fn Display() -> View {
|
|||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,16 +21,16 @@ fn RegulatorInput(regulator: Rc<dyn Regulator>) -> View {
|
|||
// get the regulator's measurement and set point signals
|
||||
let measurement = regulator.measurement();
|
||||
let set_point = regulator.set_point();
|
||||
|
||||
|
||||
// the `valid` signal tracks whether the last entered value is a valid set
|
||||
// point specification
|
||||
let valid = create_signal(true);
|
||||
|
||||
|
||||
// the `value` signal holds the current set point specification
|
||||
let value = create_signal(
|
||||
set_point.with_untracked(|set_pt| set_pt.spec.clone())
|
||||
);
|
||||
|
||||
|
||||
// this `reset_value` closure resets the input value to the regulator's set
|
||||
// point specification
|
||||
let reset_value = move || {
|
||||
|
@ -39,11 +39,11 @@ fn RegulatorInput(regulator: Rc<dyn Regulator>) -> View {
|
|||
value.set(set_point.with(|set_pt| set_pt.spec.clone()));
|
||||
})
|
||||
};
|
||||
|
||||
|
||||
// reset the input value whenever the regulator's set point specification
|
||||
// is updated
|
||||
create_effect(reset_value);
|
||||
|
||||
|
||||
view! {
|
||||
input(
|
||||
r#type = "text",
|
||||
|
@ -241,7 +241,7 @@ fn ElementOutlineItem(element: Rc<dyn Element>) -> View {
|
|||
#[component]
|
||||
pub fn Outline() -> View {
|
||||
let state = use_context::<AppState>();
|
||||
|
||||
|
||||
// list the elements alphabetically by ID
|
||||
/* TO DO */
|
||||
// this code is designed to generalize easily to other sort keys. if we only
|
||||
|
@ -254,7 +254,7 @@ pub fn Outline() -> View {
|
|||
.sorted_by_key(|elt| elt.id().clone())
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
|
||||
|
||||
view! {
|
||||
ul(
|
||||
id = "outline",
|
||||
|
@ -272,4 +272,4 @@ pub fn Outline() -> View {
|
|||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,10 +10,10 @@ out vec4 outColor;
|
|||
|
||||
void main() {
|
||||
float r = total_radius * length(2.*gl_PointCoord - vec2(1.));
|
||||
|
||||
|
||||
const float POINT_RADIUS = 4.;
|
||||
float border = smoothstep(POINT_RADIUS - 1., POINT_RADIUS, r);
|
||||
float disk = 1. - smoothstep(total_radius - 1., total_radius, r);
|
||||
vec4 color = mix(point_color, vec4(1.), border * point_highlight);
|
||||
outColor = vec4(vec3(1.), disk) * color;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,11 +14,11 @@ const float focal_slope = 0.3;
|
|||
|
||||
void main() {
|
||||
total_radius = 5. + 0.5*selected;
|
||||
|
||||
|
||||
float depth = -focal_slope * position.z;
|
||||
gl_Position = vec4(position.xy / depth, 0., 1.);
|
||||
gl_PointSize = 2.*total_radius;
|
||||
|
||||
|
||||
point_color = color;
|
||||
point_highlight = highlight;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -75,7 +75,7 @@ Fragment sphere_shading(vecInv v, vec3 pt, vec4 base_color) {
|
|||
// point. i calculated it in my head and decided that the result looked good
|
||||
// enough for now
|
||||
vec3 normal = normalize(-v.sp + 2.*v.lt.s*pt);
|
||||
|
||||
|
||||
float incidence = dot(normal, light_dir);
|
||||
float illum = mix(0.4, 1.0, max(incidence, 0.0));
|
||||
return Fragment(pt, normal, vec4(illum * base_color.rgb, base_color.a));
|
||||
|
@ -110,7 +110,7 @@ vec2 sphere_cast(vecInv v, vec3 dir) {
|
|||
float a = -v.lt.s * dot(dir, dir);
|
||||
float b = dot(v.sp, dir);
|
||||
float c = -v.lt.t;
|
||||
|
||||
|
||||
float adjust = 4.*a*c/(b*b);
|
||||
if (adjust < 1.) {
|
||||
// as long as `b` is non-zero, the linear approximation of
|
||||
|
@ -136,7 +136,7 @@ vec2 sphere_cast(vecInv v, vec3 dir) {
|
|||
void main() {
|
||||
vec2 scr = (2.*gl_FragCoord.xy - resolution) / shortdim;
|
||||
vec3 dir = vec3(focal_slope * scr, -1.);
|
||||
|
||||
|
||||
// cast rays through the spheres
|
||||
const int LAYER_MAX = 12;
|
||||
TaggedDepth top_hits [LAYER_MAX];
|
||||
|
@ -144,7 +144,7 @@ void main() {
|
|||
for (int id = 0; id < sphere_cnt; ++id) {
|
||||
// find out where the ray hits the sphere
|
||||
vec2 hit_depths = sphere_cast(sphere_list[id], dir);
|
||||
|
||||
|
||||
// insertion-sort the points we hit into the hit list
|
||||
float dimming = 1.;
|
||||
for (int side = 0; side < 2; ++side) {
|
||||
|
@ -169,14 +169,14 @@ void main() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* DEBUG */
|
||||
// in debug mode, show the layer count instead of the shaded image
|
||||
if (debug_mode) {
|
||||
// at the bottom of the screen, show the color scale instead of the
|
||||
// layer count
|
||||
if (gl_FragCoord.y < 10.) layer_cnt = int(16. * gl_FragCoord.x / resolution.x);
|
||||
|
||||
|
||||
// convert number to color
|
||||
ivec3 bits = layer_cnt / ivec3(1, 2, 4);
|
||||
vec3 color = mod(vec3(bits), 2.);
|
||||
|
@ -186,7 +186,7 @@ void main() {
|
|||
outColor = vec4(color, 1.);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// composite the sphere fragments
|
||||
vec3 color = vec3(0.);
|
||||
int layer = layer_cnt - 1;
|
||||
|
@ -203,7 +203,7 @@ void main() {
|
|||
// load the current fragment
|
||||
Fragment frag = frag_next;
|
||||
float highlight = highlight_next;
|
||||
|
||||
|
||||
// shade the next fragment
|
||||
hit = top_hits[layer];
|
||||
sphere_color = color_list[hit.id];
|
||||
|
@ -213,23 +213,23 @@ void main() {
|
|||
vec4(hit.dimming * sphere_color.rgb, sphere_color.a)
|
||||
);
|
||||
highlight_next = highlight_list[hit.id];
|
||||
|
||||
|
||||
// highlight intersections
|
||||
float ixn_dist = intersection_dist(frag, frag_next);
|
||||
float max_highlight = max(highlight, highlight_next);
|
||||
float ixn_highlight = 0.5 * max_highlight * (1. - smoothstep(2./3.*ixn_threshold, 1.5*ixn_threshold, ixn_dist));
|
||||
frag.color = mix(frag.color, vec4(1.), ixn_highlight);
|
||||
frag_next.color = mix(frag_next.color, vec4(1.), ixn_highlight);
|
||||
|
||||
|
||||
// highlight cusps
|
||||
float cusp_cos = abs(dot(dir, frag.normal));
|
||||
float cusp_threshold = 2.*sqrt(ixn_threshold * sphere_list[hit.id].lt.s);
|
||||
float cusp_highlight = highlight * (1. - smoothstep(2./3.*cusp_threshold, 1.5*cusp_threshold, cusp_cos));
|
||||
frag.color = mix(frag.color, vec4(1.), cusp_highlight);
|
||||
|
||||
|
||||
// composite the current fragment
|
||||
color = mix(color, frag.color.rgb, frag.color.a);
|
||||
}
|
||||
color = mix(color, frag_next.color.rgb, frag_next.color.a);
|
||||
outColor = vec4(sRGB(color), 1.);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -144,7 +144,7 @@ fn load_low_curvature(assembly: &Assembly) {
|
|||
engine::sphere(2.0/3.0, 4.0/3.0 * a, 0.0, 1.0/3.0),
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
// impose the desired tangencies and make the sides planar
|
||||
let index_range = 1..=3;
|
||||
let [central, assemb_plane] = ["central", "assemb_plane"].map(
|
||||
|
@ -217,7 +217,7 @@ fn load_pointed(assembly: &Assembly) {
|
|||
for index_y in 0..=1 {
|
||||
let x = index_x as f64 - 0.5;
|
||||
let y = index_y as f64 - 0.5;
|
||||
|
||||
|
||||
let _ = assembly.try_insert_element(
|
||||
Sphere::new(
|
||||
format!("sphere{index_x}{index_y}"),
|
||||
|
@ -226,7 +226,7 @@ fn load_pointed(assembly: &Assembly) {
|
|||
engine::sphere(x, y, 0.0, 1.0),
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
let _ = assembly.try_insert_element(
|
||||
Point::new(
|
||||
format!("point{index_x}{index_y}"),
|
||||
|
@ -310,7 +310,7 @@ fn load_tridiminished_icosahedron(assembly: &Assembly) {
|
|||
for vertex in vertices {
|
||||
let _ = assembly.try_insert_element(vertex);
|
||||
}
|
||||
|
||||
|
||||
// create the faces
|
||||
const COLOR_FACE: ElementColor = [0.75_f32, 0.75_f32, 0.75_f32];
|
||||
let frac_1_sqrt_6 = 1.0 / 6.0_f64.sqrt();
|
||||
|
@ -339,7 +339,7 @@ fn load_tridiminished_icosahedron(assembly: &Assembly) {
|
|||
face.ghost().set(true);
|
||||
let _ = assembly.try_insert_element(face);
|
||||
}
|
||||
|
||||
|
||||
let index_range = 1..=3;
|
||||
for j in index_range.clone() {
|
||||
// make each face planar
|
||||
|
@ -352,7 +352,7 @@ fn load_tridiminished_icosahedron(assembly: &Assembly) {
|
|||
curvature_regulator.set_point().set(
|
||||
SpecifiedValue::try_from("0".to_string()).unwrap()
|
||||
);
|
||||
|
||||
|
||||
// put each A vertex on the face it belongs to
|
||||
let vertex_a = assembly.elements_by_id.with_untracked(
|
||||
|elts_by_id| elts_by_id[&format!("a{j}")].clone()
|
||||
|
@ -360,7 +360,7 @@ fn load_tridiminished_icosahedron(assembly: &Assembly) {
|
|||
let incidence_a = InversiveDistanceRegulator::new([face.clone(), vertex_a.clone()]);
|
||||
incidence_a.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap());
|
||||
assembly.insert_regulator(Rc::new(incidence_a));
|
||||
|
||||
|
||||
// regulate the B-C vertex distances
|
||||
let vertices_bc = ["b", "c"].map(
|
||||
|series| assembly.elements_by_id.with_untracked(
|
||||
|
@ -370,10 +370,10 @@ fn load_tridiminished_icosahedron(assembly: &Assembly) {
|
|||
assembly.insert_regulator(
|
||||
Rc::new(InversiveDistanceRegulator::new(vertices_bc))
|
||||
);
|
||||
|
||||
|
||||
// get the pair of indices adjacent to `j`
|
||||
let adjacent_indices = [j % 3 + 1, (j + 1) % 3 + 1];
|
||||
|
||||
|
||||
for k in adjacent_indices.clone() {
|
||||
for series in ["b", "c"] {
|
||||
// put each B and C vertex on the faces it belongs to
|
||||
|
@ -383,14 +383,14 @@ fn load_tridiminished_icosahedron(assembly: &Assembly) {
|
|||
let incidence = InversiveDistanceRegulator::new([face.clone(), vertex.clone()]);
|
||||
incidence.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap());
|
||||
assembly.insert_regulator(Rc::new(incidence));
|
||||
|
||||
|
||||
// regulate the A-B and A-C vertex distances
|
||||
assembly.insert_regulator(
|
||||
Rc::new(InversiveDistanceRegulator::new([vertex_a.clone(), vertex]))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// regulate the A-A and C-C vertex distances
|
||||
let adjacent_pairs = ["a", "c"].map(
|
||||
|series| adjacent_indices.map(
|
||||
|
@ -422,14 +422,14 @@ fn load_dodecahedral_packing(assembly: &Assembly) {
|
|||
let substrate = assembly.elements_by_id.with_untracked(
|
||||
|elts_by_id| elts_by_id["substrate"].clone()
|
||||
);
|
||||
|
||||
|
||||
// fix the substrate's curvature
|
||||
substrate.regulators().with_untracked(
|
||||
|regs| regs.first().unwrap().clone()
|
||||
).set_point().set(
|
||||
SpecifiedValue::try_from("0.5".to_string()).unwrap()
|
||||
);
|
||||
|
||||
|
||||
// add the circles to be packed
|
||||
const COLOR_A: ElementColor = [1.00_f32, 0.25_f32, 0.00_f32];
|
||||
const COLOR_B: ElementColor = [1.00_f32, 0.00_f32, 0.25_f32];
|
||||
|
@ -445,10 +445,10 @@ fn load_dodecahedral_packing(assembly: &Assembly) {
|
|||
for k in 0..2 {
|
||||
let small_coord = face_scales[k] * (2.0*(j as f64) - 1.0);
|
||||
let big_coord = face_scales[k] * (2.0*(k as f64) - 1.0) * phi;
|
||||
|
||||
|
||||
let id_num = format!("{j}{k}");
|
||||
let label_sub = format!("{}{}", subscripts[j], subscripts[k]);
|
||||
|
||||
|
||||
// add the A face
|
||||
let id_a = format!("a{id_num}");
|
||||
let _ = assembly.try_insert_element(
|
||||
|
@ -464,7 +464,7 @@ fn load_dodecahedral_packing(assembly: &Assembly) {
|
|||
|elts_by_id| elts_by_id[&id_a].clone()
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
// add the B face
|
||||
let id_b = format!("b{id_num}");
|
||||
let _ = assembly.try_insert_element(
|
||||
|
@ -480,7 +480,7 @@ fn load_dodecahedral_packing(assembly: &Assembly) {
|
|||
|elts_by_id| elts_by_id[&id_b].clone()
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
// add the C face
|
||||
let id_c = format!("c{id_num}");
|
||||
let _ = assembly.try_insert_element(
|
||||
|
@ -498,14 +498,14 @@ fn load_dodecahedral_packing(assembly: &Assembly) {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// make each face sphere perpendicular to the substrate
|
||||
for face in faces {
|
||||
let right_angle = InversiveDistanceRegulator::new([face, substrate.clone()]);
|
||||
right_angle.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap());
|
||||
assembly.insert_regulator(Rc::new(right_angle));
|
||||
}
|
||||
|
||||
|
||||
// set up the tangencies that define the packing
|
||||
for [long_edge_plane, short_edge_plane] in [["a", "b"], ["b", "c"], ["c", "a"]] {
|
||||
for k in 0..2 {
|
||||
|
@ -524,14 +524,14 @@ fn load_dodecahedral_packing(assembly: &Assembly) {
|
|||
)
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
// set up the short-edge tangency
|
||||
let short_tangency = InversiveDistanceRegulator::new(short_edge.clone());
|
||||
if k == 0 {
|
||||
short_tangency.set_point.set(SpecifiedValue::try_from("-1".to_string()).unwrap());
|
||||
}
|
||||
assembly.insert_regulator(Rc::new(short_tangency));
|
||||
|
||||
|
||||
// set up the side tangencies
|
||||
for i in 0..2 {
|
||||
for j in 0..2 {
|
||||
|
@ -577,14 +577,14 @@ fn load_balanced(assembly: &Assembly) {
|
|||
for sphere in spheres {
|
||||
let _ = assembly.try_insert_element(sphere);
|
||||
}
|
||||
|
||||
|
||||
// get references to the spheres
|
||||
let [outer, a, b] = ["outer", "a", "b"].map(
|
||||
|id| assembly.elements_by_id.with_untracked(
|
||||
|elts_by_id| elts_by_id[id].clone()
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
// fix the diameters of the outer, sun, and moon spheres
|
||||
for (sphere, radius) in [
|
||||
(outer.clone(), R_OUTER),
|
||||
|
@ -599,7 +599,7 @@ fn load_balanced(assembly: &Assembly) {
|
|||
SpecifiedValue::try_from(curvature.to_string()).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// set the inversive distances between the spheres. as described above, the
|
||||
// initial configuration deliberately violates these constraints
|
||||
for inner in [a, b] {
|
||||
|
@ -629,14 +629,14 @@ fn load_off_center(assembly: &Assembly) {
|
|||
engine::sphere(0.0, 0.0, 0.0, 1.0),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
// get references to the elements
|
||||
let point_and_sphere = ["point", "sphere"].map(
|
||||
|id| assembly.elements_by_id.with_untracked(
|
||||
|elts_by_id| elts_by_id[id].clone()
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
// put the point on the sphere
|
||||
let incidence = InversiveDistanceRegulator::new(point_and_sphere);
|
||||
incidence.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap());
|
||||
|
@ -650,7 +650,7 @@ fn load_off_center(assembly: &Assembly) {
|
|||
// inversive distance of 0 between the circumsphere and each vertex
|
||||
fn load_radius_ratio(assembly: &Assembly) {
|
||||
let index_range = 1..=4;
|
||||
|
||||
|
||||
// create the spheres
|
||||
const GRAY: ElementColor = [0.75_f32, 0.75_f32, 0.75_f32];
|
||||
let spheres = [
|
||||
|
@ -670,7 +670,7 @@ fn load_radius_ratio(assembly: &Assembly) {
|
|||
for sphere in spheres {
|
||||
let _ = assembly.try_insert_element(sphere);
|
||||
}
|
||||
|
||||
|
||||
// create the vertices
|
||||
let vertices = izip!(
|
||||
index_range.clone(),
|
||||
|
@ -699,7 +699,7 @@ fn load_radius_ratio(assembly: &Assembly) {
|
|||
for vertex in vertices {
|
||||
let _ = assembly.try_insert_element(vertex);
|
||||
}
|
||||
|
||||
|
||||
// create the faces
|
||||
let base_dir = Vector3::new(1.0, 0.75, 1.0).normalize();
|
||||
let offset = base_dir.dot(&Vector3::new(-0.6, 0.8, 0.6));
|
||||
|
@ -731,7 +731,7 @@ fn load_radius_ratio(assembly: &Assembly) {
|
|||
face.ghost().set(true);
|
||||
let _ = assembly.try_insert_element(face);
|
||||
}
|
||||
|
||||
|
||||
// impose the constraints
|
||||
for j in index_range.clone() {
|
||||
let [face_j, vertex_j] = [
|
||||
|
@ -742,7 +742,7 @@ fn load_radius_ratio(assembly: &Assembly) {
|
|||
|elts_by_id| elts_by_id[&id].clone()
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
// make the faces planar
|
||||
let curvature_regulator = face_j.regulators().with_untracked(
|
||||
|regs| regs.first().unwrap().clone()
|
||||
|
@ -750,12 +750,12 @@ fn load_radius_ratio(assembly: &Assembly) {
|
|||
curvature_regulator.set_point().set(
|
||||
SpecifiedValue::try_from("0".to_string()).unwrap()
|
||||
);
|
||||
|
||||
|
||||
for k in index_range.clone().filter(|&index| index != j) {
|
||||
let vertex_k = assembly.elements_by_id.with_untracked(
|
||||
|elts_by_id| elts_by_id[&format!("v{k}")].clone()
|
||||
);
|
||||
|
||||
|
||||
// fix the distances between the vertices
|
||||
if j < k {
|
||||
let distance_regulator = InversiveDistanceRegulator::new(
|
||||
|
@ -763,7 +763,7 @@ fn load_radius_ratio(assembly: &Assembly) {
|
|||
);
|
||||
assembly.insert_regulator(Rc::new(distance_regulator));
|
||||
}
|
||||
|
||||
|
||||
// put the vertices on the faces
|
||||
let incidence_regulator = InversiveDistanceRegulator::new([face_j.clone(), vertex_k.clone()]);
|
||||
incidence_regulator.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap());
|
||||
|
@ -799,7 +799,7 @@ fn load_irisawa_hexlet(assembly: &Assembly) {
|
|||
[0.00_f32, 0.25_f32, 1.00_f32],
|
||||
[0.25_f32, 0.00_f32, 1.00_f32],
|
||||
].into_iter();
|
||||
|
||||
|
||||
// create the spheres
|
||||
let spheres = [
|
||||
Sphere::new(
|
||||
|
@ -836,7 +836,7 @@ fn load_irisawa_hexlet(assembly: &Assembly) {
|
|||
for sphere in spheres {
|
||||
let _ = assembly.try_insert_element(sphere);
|
||||
}
|
||||
|
||||
|
||||
// put the outer sphere in ghost mode and fix its curvature
|
||||
let outer = assembly.elements_by_id.with_untracked(
|
||||
|elts_by_id| elts_by_id["outer"].clone()
|
||||
|
@ -848,7 +848,7 @@ fn load_irisawa_hexlet(assembly: &Assembly) {
|
|||
outer_curvature_regulator.set_point().set(
|
||||
SpecifiedValue::try_from((1.0 / 3.0).to_string()).unwrap()
|
||||
);
|
||||
|
||||
|
||||
// impose the desired tangencies
|
||||
let [outer, sun, moon] = ["outer", "sun", "moon"].map(
|
||||
|id| assembly.elements_by_id.with_untracked(
|
||||
|
@ -872,11 +872,11 @@ fn load_irisawa_hexlet(assembly: &Assembly) {
|
|||
assembly.insert_regulator(Rc::new(tangency));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let outer_sun_tangency = InversiveDistanceRegulator::new([outer.clone(), sun]);
|
||||
outer_sun_tangency.set_point.set(SpecifiedValue::try_from("1".to_string()).unwrap());
|
||||
assembly.insert_regulator(Rc::new(outer_sun_tangency));
|
||||
|
||||
|
||||
let outer_moon_tangency = InversiveDistanceRegulator::new([outer.clone(), moon]);
|
||||
outer_moon_tangency.set_point.set(SpecifiedValue::try_from("1".to_string()).unwrap());
|
||||
assembly.insert_regulator(Rc::new(outer_moon_tangency));
|
||||
|
@ -895,18 +895,18 @@ pub fn TestAssemblyChooser() -> View {
|
|||
console::log_1(
|
||||
&JsValue::from(format!("Showing assembly \"{}\"", name.clone()))
|
||||
);
|
||||
|
||||
|
||||
batch(|| {
|
||||
let state = use_context::<AppState>();
|
||||
let assembly = &state.assembly;
|
||||
|
||||
|
||||
// clear state
|
||||
assembly.regulators.update(|regs| regs.clear());
|
||||
assembly.elements.update(|elts| elts.clear());
|
||||
assembly.elements_by_id.update(|elts_by_id| elts_by_id.clear());
|
||||
assembly.descent_history.set(DescentHistory::new());
|
||||
state.selection.update(|sel| sel.clear());
|
||||
|
||||
|
||||
// load assembly
|
||||
match name.as_str() {
|
||||
"general" => load_general(assembly),
|
||||
|
@ -922,7 +922,7 @@ pub fn TestAssemblyChooser() -> View {
|
|||
};
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// build the chooser
|
||||
view! {
|
||||
select(bind:value = assembly_name) {
|
||||
|
@ -938,4 +938,4 @@ pub fn TestAssemblyChooser() -> View {
|
|||
option(value = "empty") { "Empty" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,19 +62,19 @@ impl PartialMatrix {
|
|||
pub fn new() -> Self {
|
||||
Self(Vec::<MatrixEntry>::new())
|
||||
}
|
||||
|
||||
|
||||
pub fn push(&mut self, row: usize, col: usize, value: f64) {
|
||||
let Self(entries) = self;
|
||||
entries.push(MatrixEntry { index: (row, col), value });
|
||||
}
|
||||
|
||||
|
||||
pub fn push_sym(&mut self, row: usize, col: usize, value: f64) {
|
||||
self.push(row, col, value);
|
||||
if row != col {
|
||||
self.push(col, row, value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn freeze(&self, a: &DMatrix<f64>) -> DMatrix<f64> {
|
||||
let mut result = a.clone();
|
||||
for &MatrixEntry { index, value } in self {
|
||||
|
@ -82,7 +82,7 @@ impl PartialMatrix {
|
|||
}
|
||||
result
|
||||
}
|
||||
|
||||
|
||||
fn proj(&self, a: &DMatrix<f64>) -> DMatrix<f64> {
|
||||
let mut result = DMatrix::<f64>::zeros(a.nrows(), a.ncols());
|
||||
for &MatrixEntry { index, .. } in self {
|
||||
|
@ -90,7 +90,7 @@ impl PartialMatrix {
|
|||
}
|
||||
result
|
||||
}
|
||||
|
||||
|
||||
fn sub_proj(&self, rhs: &DMatrix<f64>) -> DMatrix<f64> {
|
||||
let mut result = DMatrix::<f64>::zeros(rhs.nrows(), rhs.ncols());
|
||||
for &MatrixEntry { index, value } in self {
|
||||
|
@ -112,7 +112,7 @@ impl Display for PartialMatrix {
|
|||
impl IntoIterator for PartialMatrix {
|
||||
type Item = MatrixEntry;
|
||||
type IntoIter = std::vec::IntoIter<Self::Item>;
|
||||
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
let Self(entries) = self;
|
||||
entries.into_iter()
|
||||
|
@ -122,7 +122,7 @@ impl IntoIterator for PartialMatrix {
|
|||
impl<'a> IntoIterator for &'a PartialMatrix {
|
||||
type Item = &'a MatrixEntry;
|
||||
type IntoIter = std::slice::Iter<'a, MatrixEntry>;
|
||||
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
let PartialMatrix(entries) = self;
|
||||
entries.into_iter()
|
||||
|
@ -146,7 +146,7 @@ impl ConfigSubspace {
|
|||
basis_std: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// approximate the kernel of a symmetric endomorphism of the configuration
|
||||
// space for `assembly_dim` elements. we consider an eigenvector to be part
|
||||
// of the kernel if its eigenvalue is smaller than the constant `THRESHOLD`
|
||||
|
@ -167,10 +167,10 @@ impl ConfigSubspace {
|
|||
|(λ, v)| (λ.abs() < THRESHOLD).then_some(v)
|
||||
).collect::<Vec<_>>().as_slice()
|
||||
);
|
||||
|
||||
|
||||
// express the basis in the standard coordinates
|
||||
let basis_std = proj_to_std * &basis_proj;
|
||||
|
||||
|
||||
const ELEMENT_DIM: usize = 5;
|
||||
const UNIFORM_DIM: usize = 4;
|
||||
Self {
|
||||
|
@ -187,15 +187,15 @@ impl ConfigSubspace {
|
|||
).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn dim(&self) -> usize {
|
||||
self.basis_std.len()
|
||||
}
|
||||
|
||||
|
||||
pub fn assembly_dim(&self) -> usize {
|
||||
self.assembly_dim
|
||||
}
|
||||
|
||||
|
||||
// find the projection onto this subspace of the motion where the element
|
||||
// with the given column index has velocity `v`. the velocity is given in
|
||||
// projection coordinates, and the projection is done with respect to the
|
||||
|
@ -253,7 +253,7 @@ impl ConstraintProblem {
|
|||
guess: DMatrix::<f64>::zeros(ELEMENT_DIM, element_count),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(feature = "dev")]
|
||||
pub fn from_guess(guess_columns: &[DVector<f64>]) -> Self {
|
||||
Self {
|
||||
|
@ -377,10 +377,10 @@ pub fn realize_gram(
|
|||
) -> Realization {
|
||||
// destructure the problem data
|
||||
let ConstraintProblem { gram, guess, frozen } = problem;
|
||||
|
||||
|
||||
// start the descent history
|
||||
let mut history = DescentHistory::new();
|
||||
|
||||
|
||||
// handle the case where the assembly is empty. our general realization
|
||||
// routine can't handle this case because it builds the Hessian using
|
||||
// `DMatrix::from_columns`, which panics when the list of columns is empty
|
||||
|
@ -394,20 +394,20 @@ pub fn realize_gram(
|
|||
);
|
||||
return Realization { result, history };
|
||||
}
|
||||
|
||||
|
||||
// find the dimension of the search space
|
||||
let element_dim = guess.nrows();
|
||||
let total_dim = element_dim * assembly_dim;
|
||||
|
||||
|
||||
// scale the tolerance
|
||||
let scale_adjustment = (gram.0.len() as f64).sqrt();
|
||||
let tol = scale_adjustment * scaled_tol;
|
||||
|
||||
|
||||
// convert the frozen indices to stacked format
|
||||
let frozen_stacked: Vec<usize> = frozen.into_iter().map(
|
||||
|MatrixEntry { index: (row, col), .. }| col*element_dim + row
|
||||
).collect();
|
||||
|
||||
|
||||
// use a regularized Newton's method with backtracking
|
||||
let mut state = SearchState::from_config(gram, frozen.freeze(guess));
|
||||
let mut hess = DMatrix::zeros(element_dim, assembly_dim);
|
||||
|
@ -416,7 +416,7 @@ pub fn realize_gram(
|
|||
let neg_grad = 4.0 * &*Q * &state.config * &state.err_proj;
|
||||
let mut neg_grad_stacked = neg_grad.clone().reshape_generic(Dyn(total_dim), Const::<1>);
|
||||
history.neg_grad.push(neg_grad.clone());
|
||||
|
||||
|
||||
// find the negative Hessian of the loss function
|
||||
let mut hess_cols = Vec::<DVector<f64>>::with_capacity(total_dim);
|
||||
for col in 0..assembly_dim {
|
||||
|
@ -435,7 +435,7 @@ pub fn realize_gram(
|
|||
}
|
||||
}
|
||||
hess = DMatrix::from_columns(hess_cols.as_slice());
|
||||
|
||||
|
||||
// regularize the Hessian
|
||||
let hess_eigvals = hess.symmetric_eigenvalues();
|
||||
let min_eigval = hess_eigvals.min();
|
||||
|
@ -443,7 +443,7 @@ pub fn realize_gram(
|
|||
hess -= reg_scale * min_eigval * DMatrix::identity(total_dim, total_dim);
|
||||
}
|
||||
history.hess_eigvals.push(hess_eigvals);
|
||||
|
||||
|
||||
// project the negative gradient and negative Hessian onto the
|
||||
// orthogonal complement of the frozen subspace
|
||||
let zero_col = DVector::zeros(total_dim);
|
||||
|
@ -454,12 +454,12 @@ pub fn realize_gram(
|
|||
hess.set_column(k, &zero_col);
|
||||
hess[(k, k)] = 1.0;
|
||||
}
|
||||
|
||||
|
||||
// stop if the loss is tolerably low
|
||||
history.config.push(state.config.clone());
|
||||
history.scaled_loss.push(state.loss / scale_adjustment);
|
||||
if state.loss < tol { break; }
|
||||
|
||||
|
||||
// compute the Newton step
|
||||
/* TO DO */
|
||||
/*
|
||||
|
@ -479,7 +479,7 @@ pub fn realize_gram(
|
|||
let base_step_stacked = hess_cholesky.solve(&neg_grad_stacked);
|
||||
let base_step = base_step_stacked.reshape_generic(Dyn(element_dim), Dyn(assembly_dim));
|
||||
history.base_step.push(base_step.clone());
|
||||
|
||||
|
||||
// use backtracking line search to find a better configuration
|
||||
if let Some((better_state, backoff_steps)) = seek_better_config(
|
||||
gram, &state, &base_step, neg_grad.dot(&base_step),
|
||||
|
@ -505,10 +505,10 @@ pub fn realize_gram(
|
|||
.view_mut(block_start, (element_dim, UNIFORM_DIM))
|
||||
.copy_from(&local_unif_to_std(state.config.column(n)));
|
||||
}
|
||||
|
||||
|
||||
// find the kernel of the Hessian. give it the uniform inner product
|
||||
let tangent = ConfigSubspace::symmetric_kernel(hess, unif_to_std, assembly_dim);
|
||||
|
||||
|
||||
Ok(ConfigNeighborhood { #[cfg(feature = "dev")] config: state.config, nbhd: tangent })
|
||||
} else {
|
||||
Err("Failed to reach target accuracy".to_string())
|
||||
|
@ -521,9 +521,9 @@ pub fn realize_gram(
|
|||
#[cfg(feature = "dev")]
|
||||
pub mod examples {
|
||||
use std::f64::consts::PI;
|
||||
|
||||
|
||||
use super::*;
|
||||
|
||||
|
||||
// this problem is from a sangaku by Irisawa Shintarō Hiroatsu. the article
|
||||
// below includes a nice translation of the problem statement, which was
|
||||
// recorded in Uchida Itsumi's book _Kokon sankan_ (_Mathematics, Past and
|
||||
|
@ -547,40 +547,40 @@ pub mod examples {
|
|||
)
|
||||
).collect::<Vec<_>>().as_slice()
|
||||
);
|
||||
|
||||
|
||||
for s in 0..9 {
|
||||
// each sphere is represented by a spacelike vector
|
||||
problem.gram.push_sym(s, s, 1.0);
|
||||
|
||||
|
||||
// the circumscribing sphere is tangent to all of the other
|
||||
// spheres, with matching orientation
|
||||
if s > 0 {
|
||||
problem.gram.push_sym(0, s, 1.0);
|
||||
}
|
||||
|
||||
|
||||
if s > 2 {
|
||||
// each chain sphere is tangent to the "sun" and "moon"
|
||||
// spheres, with opposing orientation
|
||||
for n in 1..3 {
|
||||
problem.gram.push_sym(s, n, -1.0);
|
||||
}
|
||||
|
||||
|
||||
// each chain sphere is tangent to the next chain sphere,
|
||||
// with opposing orientation
|
||||
let s_next = 3 + (s-2) % 6;
|
||||
problem.gram.push_sym(s, s_next, -1.0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// the frozen entries fix the radii of the circumscribing sphere, the
|
||||
// "sun" and "moon" spheres, and one of the chain spheres
|
||||
for k in 0..4 {
|
||||
problem.frozen.push(3, k, problem.guess[(3, k)]);
|
||||
}
|
||||
|
||||
|
||||
realize_gram(&problem, scaled_tol, 0.5, 0.9, 1.1, 200, 110)
|
||||
}
|
||||
|
||||
|
||||
// set up a kaleidocycle, made of points with fixed distances between them,
|
||||
// and find its tangent space
|
||||
pub fn realize_kaleidocycle(scaled_tol: f64) -> Realization {
|
||||
|
@ -601,7 +601,7 @@ pub mod examples {
|
|||
}
|
||||
).collect::<Vec<_>>().as_slice()
|
||||
);
|
||||
|
||||
|
||||
const N_POINTS: usize = 2 * N_HINGES;
|
||||
for block in (0..N_POINTS).step_by(2) {
|
||||
let block_next = (block + 2) % N_POINTS;
|
||||
|
@ -610,18 +610,18 @@ pub mod examples {
|
|||
for k in j..2 {
|
||||
problem.gram.push_sym(block + j, block + k, if j == k { 0.0 } else { -0.5 });
|
||||
}
|
||||
|
||||
|
||||
// non-hinge edges
|
||||
for k in 0..2 {
|
||||
problem.gram.push_sym(block + j, block_next + k, -0.625);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for k in 0..N_POINTS {
|
||||
problem.frozen.push(3, k, problem.guess[(3, k)])
|
||||
}
|
||||
|
||||
|
||||
realize_gram(&problem, scaled_tol, 0.5, 0.9, 1.1, 200, 110)
|
||||
}
|
||||
}
|
||||
|
@ -630,9 +630,9 @@ pub mod examples {
|
|||
mod tests {
|
||||
use nalgebra::Vector3;
|
||||
use std::{f64::consts::{FRAC_1_SQRT_2, PI}, iter};
|
||||
|
||||
|
||||
use super::{*, examples::*};
|
||||
|
||||
|
||||
#[test]
|
||||
fn freeze_test() {
|
||||
let frozen = PartialMatrix(vec![
|
||||
|
@ -651,7 +651,7 @@ mod tests {
|
|||
]);
|
||||
assert_eq!(frozen.freeze(&config), expected_result);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn sub_proj_test() {
|
||||
let target = PartialMatrix(vec![
|
||||
|
@ -670,7 +670,7 @@ mod tests {
|
|||
]);
|
||||
assert_eq!(target.sub_proj(&attempt), expected_result);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn zero_loss_test() {
|
||||
let mut gram = PartialMatrix::new();
|
||||
|
@ -690,7 +690,7 @@ mod tests {
|
|||
let state = SearchState::from_config(&gram, config);
|
||||
assert!(state.loss.abs() < f64::EPSILON);
|
||||
}
|
||||
|
||||
|
||||
/* TO DO */
|
||||
// at the frozen indices, the optimization steps should have exact zeros,
|
||||
// and the realized configuration should have the desired values
|
||||
|
@ -720,13 +720,13 @@ mod tests {
|
|||
assert_eq!(config[index], value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn irisawa_hexlet_test() {
|
||||
// solve Irisawa's problem
|
||||
const SCALED_TOL: f64 = 1.0e-12;
|
||||
let config = realize_irisawa_hexlet(SCALED_TOL).result.unwrap().config;
|
||||
|
||||
|
||||
// check against Irisawa's solution
|
||||
let entry_tol = SCALED_TOL.sqrt();
|
||||
let solution_diams = [30.0, 10.0, 6.0, 5.0, 15.0, 10.0, 3.75, 2.5, 2.0 + 8.0/11.0];
|
||||
|
@ -734,7 +734,7 @@ mod tests {
|
|||
assert!((config[(3, k)] - 1.0 / diam).abs() < entry_tol);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn tangent_test_three_spheres() {
|
||||
const SCALED_TOL: f64 = 1.0e-12;
|
||||
|
@ -758,7 +758,7 @@ mod tests {
|
|||
let ConfigNeighborhood { config, nbhd: tangent } = result.unwrap();
|
||||
assert_eq!(config, problem.guess);
|
||||
assert_eq!(history.scaled_loss.len(), 1);
|
||||
|
||||
|
||||
// list some motions that should form a basis for the tangent space of
|
||||
// the solution variety
|
||||
const UNIFORM_DIM: usize = 4;
|
||||
|
@ -786,11 +786,11 @@ mod tests {
|
|||
0.0, 0.0, -1.0, 0.25, 1.0,
|
||||
]),
|
||||
];
|
||||
|
||||
|
||||
// confirm that the dimension of the tangent space is no greater than
|
||||
// expected
|
||||
assert_eq!(tangent.basis_std.len(), tangent_motions_std.len());
|
||||
|
||||
|
||||
// confirm that the tangent space contains all the motions we expect it
|
||||
// to. since we've already bounded the dimension of the tangent space,
|
||||
// this confirms that the tangent space is what we expect it to be
|
||||
|
@ -802,13 +802,13 @@ mod tests {
|
|||
assert!((motion_std - motion_proj).norm_squared() < tol_sq);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn translation_motion_unif(vel: &Vector3<f64>, assembly_dim: usize) -> Vec<DVector<f64>> {
|
||||
let mut elt_motion = DVector::zeros(4);
|
||||
elt_motion.fixed_rows_mut::<3>(0).copy_from(vel);
|
||||
iter::repeat(elt_motion).take(assembly_dim).collect()
|
||||
}
|
||||
|
||||
|
||||
fn rotation_motion_unif(ang_vel: &Vector3<f64>, points: Vec<DVectorView<f64>>) -> Vec<DVector<f64>> {
|
||||
points.into_iter().map(
|
||||
|pt| {
|
||||
|
@ -819,7 +819,7 @@ mod tests {
|
|||
}
|
||||
).collect()
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn tangent_test_kaleidocycle() {
|
||||
// set up a kaleidocycle and find its tangent space
|
||||
|
@ -827,7 +827,7 @@ mod tests {
|
|||
let Realization { result, history } = realize_kaleidocycle(SCALED_TOL);
|
||||
let ConfigNeighborhood { config, nbhd: tangent } = result.unwrap();
|
||||
assert_eq!(history.scaled_loss.len(), 1);
|
||||
|
||||
|
||||
// list some motions that should form a basis for the tangent space of
|
||||
// the solution variety
|
||||
const N_HINGES: usize = 6;
|
||||
|
@ -838,12 +838,12 @@ mod tests {
|
|||
translation_motion_unif(&Vector3::new(1.0, 0.0, 0.0), assembly_dim),
|
||||
translation_motion_unif(&Vector3::new(0.0, 1.0, 0.0), assembly_dim),
|
||||
translation_motion_unif(&Vector3::new(0.0, 0.0, 1.0), assembly_dim),
|
||||
|
||||
|
||||
// the rotations about the coordinate axes
|
||||
rotation_motion_unif(&Vector3::new(1.0, 0.0, 0.0), config.column_iter().collect()),
|
||||
rotation_motion_unif(&Vector3::new(0.0, 1.0, 0.0), config.column_iter().collect()),
|
||||
rotation_motion_unif(&Vector3::new(0.0, 0.0, 1.0), config.column_iter().collect()),
|
||||
|
||||
|
||||
// the twist motion. more precisely: a motion that keeps the center
|
||||
// of mass stationary and preserves the distances between the
|
||||
// vertices to first order. this has to be the twist as long as:
|
||||
|
@ -872,11 +872,11 @@ mod tests {
|
|||
).collect::<Vec<_>>()
|
||||
)
|
||||
).collect::<Vec<_>>();
|
||||
|
||||
|
||||
// confirm that the dimension of the tangent space is no greater than
|
||||
// expected
|
||||
assert_eq!(tangent.basis_std.len(), tangent_motions_unif.len());
|
||||
|
||||
|
||||
// confirm that the tangent space contains all the motions we expect it
|
||||
// to. since we've already bounded the dimension of the tangent space,
|
||||
// this confirms that the tangent space is what we expect it to be
|
||||
|
@ -888,7 +888,7 @@ mod tests {
|
|||
assert!((motion_std - motion_proj).norm_squared() < tol_sq);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn translation(dis: Vector3<f64>) -> DMatrix<f64> {
|
||||
const ELEMENT_DIM: usize = 5;
|
||||
DMatrix::from_column_slice(ELEMENT_DIM, ELEMENT_DIM, &[
|
||||
|
@ -899,7 +899,7 @@ mod tests {
|
|||
0.0, 0.0, 0.0, 0.0, 1.0,
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
// confirm that projection onto a configuration subspace is equivariant with
|
||||
// respect to Euclidean motions
|
||||
#[test]
|
||||
|
@ -919,7 +919,7 @@ mod tests {
|
|||
let ConfigNeighborhood { config: config_orig, nbhd: tangent_orig } = result_orig.unwrap();
|
||||
assert_eq!(config_orig, problem_orig.guess);
|
||||
assert_eq!(history_orig.scaled_loss.len(), 1);
|
||||
|
||||
|
||||
// find another pair of spheres that meet at 120°. we'll think of this
|
||||
// solution as a transformed version of the original one
|
||||
let guess_tfm = {
|
||||
|
@ -940,17 +940,17 @@ mod tests {
|
|||
let ConfigNeighborhood { config: config_tfm, nbhd: tangent_tfm } = result_tfm.unwrap();
|
||||
assert_eq!(config_tfm, problem_tfm.guess);
|
||||
assert_eq!(history_tfm.scaled_loss.len(), 1);
|
||||
|
||||
|
||||
// project a nudge to the tangent space of the solution variety at the
|
||||
// original solution
|
||||
let motion_orig = DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]);
|
||||
let motion_orig_proj = tangent_orig.proj(&motion_orig.as_view(), 0);
|
||||
|
||||
|
||||
// project the equivalent nudge to the tangent space of the solution
|
||||
// variety at the transformed solution
|
||||
let motion_tfm = DVector::from_column_slice(&[FRAC_1_SQRT_2, 0.0, FRAC_1_SQRT_2, 0.0]);
|
||||
let motion_tfm_proj = tangent_tfm.proj(&motion_tfm.as_view(), 0);
|
||||
|
||||
|
||||
// take the transformation that sends the original solution to the
|
||||
// transformed solution and apply it to the motion that the original
|
||||
// solution makes in response to the nudge
|
||||
|
@ -964,7 +964,7 @@ mod tests {
|
|||
]);
|
||||
let transl = translation(Vector3::new(0.0, 0.0, 7.0));
|
||||
let motion_proj_tfm = transl * rot * motion_orig_proj;
|
||||
|
||||
|
||||
// confirm that the projection of the nudge is equivariant. we loosen
|
||||
// the comparison tolerance because the transformation seems to
|
||||
// introduce some numerical error
|
||||
|
@ -972,4 +972,4 @@ mod tests {
|
|||
let tol_sq = ((problem_orig.guess.nrows() * problem_orig.guess.ncols()) as f64) * SCALED_TOL_TFM * SCALED_TOL_TFM;
|
||||
assert!((motion_proj_tfm - motion_tfm_proj).norm_squared() < tol_sq);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ impl AppState {
|
|||
selection: create_signal(BTreeSet::default()),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// in single-selection mode, select the given element. in multiple-selection
|
||||
// mode, toggle whether the given element is selected
|
||||
fn select(&self, element: &Rc<dyn Element>, multi: bool) {
|
||||
|
@ -53,10 +53,10 @@ fn main() {
|
|||
// set the console error panic hook
|
||||
#[cfg(feature = "console_error_panic_hook")]
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
|
||||
sycamore::render(|| {
|
||||
provide_context(AppState::new());
|
||||
|
||||
|
||||
view! {
|
||||
div(id = "sidebar") {
|
||||
AddRemove {}
|
||||
|
@ -66,4 +66,4 @@ fn main() {
|
|||
Display {}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ impl SpecifiedValue {
|
|||
pub fn from_empty_spec() -> Self {
|
||||
Self { spec: String::new(), value: None }
|
||||
}
|
||||
|
||||
|
||||
pub fn is_present(&self) -> bool {
|
||||
matches!(self.value, Some(_))
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ impl From<Option<f64>> for SpecifiedValue {
|
|||
// if the specification is properly formatted, and `Error` if not
|
||||
impl TryFrom<String> for SpecifiedValue {
|
||||
type Error = ParseFloatError;
|
||||
|
||||
|
||||
fn try_from(spec: String) -> Result<Self, Self::Error> {
|
||||
if spec.is_empty() {
|
||||
Ok(Self::from_empty_spec())
|
||||
|
@ -52,4 +52,4 @@ impl TryFrom<String> for SpecifiedValue {
|
|||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue