diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs index 0d2a510..d205208 100644 --- a/app-proto/src/assembly.rs +++ b/app-proto/src/assembly.rs @@ -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) -> Vec> { Vec::new() } - + fn id(&self) -> &String; fn label(&self) -> &String; fn representation(&self) -> Signal>; fn ghost(&self) -> Signal; - + // 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>>; - + // project a representation vector for this kind of element onto its // normalization variety fn project_to_normalized(&self, rep: &mut DVector); - + // 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; - + // 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) -> Vec> { vec![Rc::new(HalfCurvatureRegulator::new(self))] } - + fn id(&self) -> &String { &self.id } - + fn label(&self) -> &String { &self.label } - + fn representation(&self) -> Signal> { self.representation } - + fn ghost(&self) -> Signal { self.ghost } - + fn regulators(&self) -> Signal>> { self.regulators } - + fn project_to_normalized(&self, rep: &mut DVector) { project_sphere_to_normalized(rep); } - + fn column_index(&self) -> Option { self.column_index.get() } - + fn set_column_index(&self, index: usize) { self.column_index.set(Some(index)); } @@ -261,7 +261,8 @@ impl ProblemPoser for Sphere { let index = self.column_index().expect( indexing_error("Sphere", &self.id, "it").as_str()); problem.gram.push_sym(index, index, 1.0); - problem.guess.set_column(index, &self.representation.get_clone_untracked()); + problem.guess.set_column( + index, &self.representation.get_clone_untracked()); } } @@ -279,7 +280,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 +304,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 +322,35 @@ impl Element for Point { }) .collect() } - + fn id(&self) -> &String { &self.id } - + fn label(&self) -> &String { &self.label } - + fn representation(&self) -> Signal> { self.representation } - + fn ghost(&self) -> Signal { self.ghost } - + fn regulators(&self) -> Signal>> { self.regulators } - + fn project_to_normalized(&self, rep: &mut DVector) { project_point_to_normalized(rep); } - + fn column_index(&self) -> Option { self.column_index.get() } - + fn set_column_index(&self, index: usize) { self.column_index.set(Some(index)); } @@ -367,7 +368,8 @@ impl ProblemPoser for Point { indexing_error("Point", &self.id, "it").as_str()); problem.gram.push_sym(index, index, 0.0); problem.frozen.push(Self::WEIGHT_COMPONENT, index, 0.5); - problem.guess.set_column(index, &self.representation.get_clone_untracked()); + problem.guess.set_column( + index, &self.representation.get_clone_untracked()); } } @@ -412,7 +414,8 @@ pub struct InversiveDistanceRegulator { impl InversiveDistanceRegulator { pub fn new(subjects: [Rc; 2]) -> Self { - let representations = subjects.each_ref().map(|subj| subj.representation()); + let representations = subjects.each_ref().map( + |subj| subj.representation()); let measurement = create_memo(move || { representations[0].with(|rep_0| representations[1].with(|rep_1| @@ -420,10 +423,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 +435,11 @@ impl Regulator for InversiveDistanceRegulator { fn subjects(&self) -> Vec> { self.subjects.clone().into() } - + fn measurement(&self) -> ReadSignal { self.measurement } - + fn set_point(&self) -> Signal { self.set_point } @@ -475,10 +478,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 +490,11 @@ impl Regulator for HalfCurvatureRegulator { fn subjects(&self) -> Vec> { vec![self.subject.clone()] } - + fn measurement(&self) -> ReadSignal { self.measurement } - + fn set_point(&self) -> Signal { self.set_point } @@ -545,7 +548,9 @@ impl PointCoordinateRegulator { move |rep| rep[axis as usize] ); let set_point = create_signal(SpecifiedValue::from_empty_spec()); - Self { subject, axis, measurement, set_point, serial: Self::next_serial() } + Self { + subject, axis, measurement, set_point, serial: Self::next_serial() + } } } @@ -579,8 +584,8 @@ impl ProblemPoser for PointCoordinateRegulator { } if nset == Axis::CARDINALITY { let [x, y, z] = coords; - problem.frozen.push( - Point::NORM_COMPONENT, col, point(x,y,z)[Point::NORM_COMPONENT]); + problem.frozen.push(Point::NORM_COMPONENT, + col, point(x,y,z)[Point::NORM_COMPONENT]); } } }); @@ -601,7 +606,7 @@ pub struct Assembly { // elements and regulators pub elements: Signal>>, pub regulators: Signal>>, - + // 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 @@ -613,13 +618,13 @@ pub struct Assembly { // in that column of the tangent space basis matrices // pub tangent: Signal, - + // indexing pub elements_by_id: Signal>>, - + // realization control pub realization_trigger: Signal<()>, - + // realization diagnostics pub realization_status: Signal>, pub descent_history: Signal, @@ -639,7 +644,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(); @@ -653,7 +658,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(); @@ -665,12 +670,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,22 +684,24 @@ impl Assembly { let id = elt.id().clone(); 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())); - + 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())); + let can_insert = self.elements_by_id.with_untracked( + |elts_by_id| !elts_by_id.contains_key(elt.id())); if can_insert { self.insert_element_unchecked(elt); } can_insert } - + pub fn insert_element_default(&self) { // find the next unused identifier in the default sequence let default_id = T::default_id(); @@ -706,17 +713,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) { // 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() @@ -724,7 +731,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:"); @@ -747,19 +754,20 @@ impl Assembly { } }); } - + // --- updating the configuration --- - + pub fn load_config(&self, config: &DMatrix) { for elt in self.elements.get_clone_untracked() { elt.representation().update( - |rep| rep.set_column(0, &config.column(elt.column_index().unwrap())) + |rep| rep.set_column( + 0, &config.column(elt.column_index().unwrap())) ); } } - + // --- realization --- - + pub fn realize(&self) { // index the elements self.elements.update_silent(|elts| { @@ -767,7 +775,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()); @@ -781,21 +789,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 { @@ -807,20 +815,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 { @@ -830,7 +838,7 @@ impl Assembly { SpecifiedValue::from_empty_spec() } ); - + // save the tangent space self.tangent.set_silent(tangent); }, @@ -840,15 +848,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: @@ -865,7 +873,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. @@ -883,7 +891,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 @@ -894,11 +902,12 @@ 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 - let mut target_columns = motion_proj.columns_mut(0, realized_dim); + let mut target_columns = + motion_proj.columns_mut(0, realized_dim); target_columns += self.tangent.with( |tan| tan.proj(&elt_motion.velocity, column_index) ); @@ -906,13 +915,14 @@ impl Assembly { // this element didn't have a column index when we started, so // by invariant (2), it's unconstrained let mut target_column = motion_proj.column_mut(column_index); - let unif_to_std = elt_motion.element.representation().with_untracked( - |rep| local_unif_to_std(rep.as_view()) - ); + let unif_to_std = + elt_motion.element.representation().with_untracked( + |rep| local_unif_to_std(rep.as_view()) + ); 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() { @@ -925,12 +935,15 @@ impl Assembly { elt.project_to_normalized(rep); }, None => { - console_log!("No velocity to unpack for fresh element \"{}\"", elt.id()) + console_log!( + "No velocity to unpack for fresh element \"{}\"", + elt.id() + ) }, }; }); } - + // 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 @@ -941,9 +954,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")] @@ -953,25 +966,27 @@ mod tests { elt.pose(&mut ConstraintProblem::new(1)); }); } - + #[test] #[should_panic(expected = "Subject \"sphere1\" must be indexed before \ inversive distance regulator writes problem data")] fn unindexed_subject_test_inversive_distance() { let _ = create_root(|| { let subjects = [0, 1].map( - |k| Rc::new(Sphere::default(format!("sphere{k}"), k)) as Rc + |k| Rc::new( + Sphere::default(format!("sphere{k}"), k)) as Rc ); subjects[0].set_column_index(0); InversiveDistanceRegulator { subjects: subjects, measurement: create_memo(|| 0.0), - set_point: create_signal(SpecifiedValue::try_from("0.0".to_string()).unwrap()), + set_point: create_signal( + SpecifiedValue::try_from("0.0".to_string()).unwrap()), serial: InversiveDistanceRegulator::next_serial() }.pose(&mut ConstraintProblem::new(2)); }); } - + #[test] fn curvature_drift_test() { const INITIAL_RADIUS: f64 = 0.25; @@ -991,12 +1006,14 @@ inversive distance regulator writes problem data")] 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; - let sphere = assembly.elements_by_id.with(|elts_by_id| elts_by_id[sphere_id].clone()); - let velocity = DVector::from_column_slice(&[0.0, 0.0, STEP_SIZE, 0.0]); + let sphere = assembly.elements_by_id.with( + |elts_by_id| elts_by_id[sphere_id].clone()); + let velocity = + DVector::from_column_slice(&[0.0, 0.0, STEP_SIZE, 0.0]); for _ in 0..STEP_CNT { assembly.deform( vec![ @@ -1007,14 +1024,15 @@ inversive distance regulator writes problem data")] ] ); } - + // check how much the sphere's curvature has drifted const INITIAL_HALF_CURV: f64 = 0.5 / INITIAL_RADIUS; const DRIFT_TOL: f64 = 0.015; let final_half_curv = sphere.representation().with_untracked( |rep| rep[Sphere::CURVATURE_COMPONENT] ); - assert!((final_half_curv / INITIAL_HALF_CURV - 1.0).abs() < DRIFT_TOL); + assert!((final_half_curv / INITIAL_HALF_CURV - 1.0).abs() + < DRIFT_TOL); }); } } diff --git a/app-proto/src/components/add_remove.rs b/app-proto/src/components/add_remove.rs index 4196640..f524d66 100644 --- a/app-proto/src/components/add_remove.rs +++ b/app-proto/src/components/add_remove.rs @@ -39,7 +39,9 @@ pub fn AddRemove() -> View { } ) { "Add point" } button( - class = "emoji", /* KLUDGE */ // for convenience, we're using an emoji as a temporary icon for this button + /* KLUDGE */ // for convenience, we're using an emoji as an + // icon for this button + class = "emoji", disabled = { let state = use_context::(); state.selection.with(|sel| sel.len() != 2) diff --git a/app-proto/src/components/diagnostics.rs b/app-proto/src/components/diagnostics.rs index 51d58f1..0909f45 100644 --- a/app-proto/src/components/diagnostics.rs +++ b/app-proto/src/components/diagnostics.rs @@ -54,7 +54,7 @@ fn StepInput() -> View { // get the assembly let state = use_context::(); 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::(); 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::(); 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 {} } } } -} \ No newline at end of file +} diff --git a/app-proto/src/components/display.rs b/app-proto/src/components/display.rs index 98be85e..f261233 100644 --- a/app-proto/src/components/display.rs +++ b/app-proto/src/components/display.rs @@ -48,11 +48,12 @@ 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") + self.representations.len().try_into().expect( + "Number of spheres must fit in a 32-bit integer") } - + fn push( &mut self, representation: DVector, color: ElementColor, opacity: f32, highlight: f32, @@ -79,7 +80,7 @@ impl ScenePoints { selections: Vec::new(), } } - + fn push( &mut self, representation: DVector, color: ElementColor, opacity: f32, highlight: f32, selected: bool, @@ -107,7 +108,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 +126,18 @@ 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 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 +149,13 @@ 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 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 +190,16 @@ 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 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, @@ -199,20 +207,21 @@ impl DisplayItem for Point { assembly_to_world: &DMatrix, pixel_size: f64, ) -> Option { - let rep = self.representation.with_untracked(|rep| assembly_to_world * rep); + let rep = self.representation.with_untracked( + |rep| assembly_to_world * rep); 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 +263,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 +282,7 @@ fn set_up_program( "Linking failed" }; console::log_1(&JsValue::from(link_msg)); - + program } @@ -318,7 +327,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 +341,7 @@ fn load_new_buffer( WebGl2RenderingContext::STATIC_DRAW, ); } - + buffer } @@ -353,15 +362,16 @@ fn event_dir(event: &MouseEvent) -> (Vector3, 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; - + let horizontal = f64::from(event.client_x()) - rect.left(); + let vertical = rect.bottom() - f64::from(event.client_y()); ( Vector3::new( - FOCAL_SLOPE * (2.0*(f64::from(event.client_x()) - rect.left()) - width) / shortdim, - FOCAL_SLOPE * (2.0*(rect.bottom() - f64::from(event.client_y())) - height) / shortdim, + FOCAL_SLOPE * (2.0*horizontal - width) / shortdim, + FOCAL_SLOPE * (2.0*vertical - height) / shortdim, -1.0, ), FOCAL_SLOPE * 2.0 / shortdim, @@ -373,13 +383,13 @@ fn event_dir(event: &MouseEvent) -> (Vector3, f64) { #[component] pub fn Display() -> View { let state = use_context::(); - + // canvas let display = create_node_ref(); - + // viewpoint let assembly_to_world = create_signal(DMatrix::::identity(5, 5)); - + // navigation let pitch_up = create_signal(0.0); let pitch_down = create_signal(0.0); @@ -390,7 +400,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 +410,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 +423,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,48 +442,50 @@ pub fn Display() -> View { let mut orientation = DMatrix::::identity(5, 5); let mut rotation = DMatrix::::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::(); + let canvas = + display.get().unchecked_into::(); let ctx = canvas .get_context("webgl2") .unwrap() .unwrap() .dyn_into::() .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); - + 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 @@ -487,16 +499,20 @@ pub fn Display() -> View { // machine, the the length of a float or genType array seems to be // capped at 1024 elements console::log_2( - &ctx.get_parameter(WebGl2RenderingContext::MAX_FRAGMENT_UNIFORM_VECTORS).unwrap(), + &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; - + 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"); + let sphere_cnt_loc = ctx.get_uniform_location( + &sphere_program, "sphere_cnt" + ); let sphere_sp_locs = get_uniform_array_locations::( &ctx, &sphere_program, "sphere_list", Some("sp") ); @@ -509,11 +525,19 @@ pub fn Display() -> View { let sphere_highlight_locs = get_uniform_array_locations::( &ctx, &sphere_program, "highlight_list", None ); - let resolution_loc = ctx.get_uniform_location(&sphere_program, "resolution"); - 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"); - + let resolution_loc = ctx.get_uniform_location( + &sphere_program, "resolution" + ); + 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] = [ @@ -526,21 +550,26 @@ pub fn Display() -> View { 1.0, 1.0, 0.0, 1.0, -1.0, 0.0, ]; - let viewport_position_buffer = load_new_buffer(&ctx, &viewport_positions); - + 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; - + 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 +580,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 +590,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 +611,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 @@ -596,7 +625,8 @@ pub fn Display() -> View { let realization_successful = state.assembly.realization_status.with( |status| status.is_ok() ); - let step_val = state.assembly.step.with_untracked(|step| step.value); + let step_val = + state.assembly.step.with_untracked(|step| step.value); let on_init_step = step_val.is_some_and(|n| n == 0.0); let on_last_step = step_val.is_some_and( |n| state.assembly.descent_history.with_untracked( @@ -606,7 +636,8 @@ pub fn Display() -> View { let on_manipulable_step = !realization_successful && on_init_step || realization_successful && on_last_step; - if on_manipulable_step && state.selection.with(|sel| sel.len() == 1) { + if on_manipulable_step + && state.selection.with(|sel| sel.len() == 1) { let sel = state.selection.with( |sel| sel.into_iter().next().unwrap().clone() ); @@ -642,24 +673,25 @@ 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; if frames_since_last_sample >= SAMPLE_PERIOD { - mean_frame_interval.set((time - last_sample_time) / (SAMPLE_PERIOD as f64)); + mean_frame_interval.set( + (time - last_sample_time) / (SAMPLE_PERIOD as f64)); 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,35 +704,37 @@ pub fn Display() -> View { ]) }; let asm_to_world = &location * &orientation; - + // set up the scene state.assembly.elements.with_untracked( |elts| for elt in elts { - let selected = state.selection.with(|sel| sel.contains(elt)); + let selected = + state.selection.with(|sel| sel.contains(elt)); elt.show(&mut scene, selected); } ); 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::() - ).collect(); - + let sphere_reps_world: Vec<_> = + scene.spheres.representations.into_iter().map( + |rep| (&asm_to_world * rep).cast::() + ).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 +756,35 @@ 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); - + 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); - + 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,30 +792,36 @@ pub fn Display() -> View { |rep| &asm_to_world_sp * rep ).collect::>().as_slice() ).cast::(); - + // load the point positions and colors into new buffers and // bind them to the corresponding attributes in the vertex // shader - bind_new_buffer_to_attribute(&ctx, point_position_attr, SPACE_DIM as i32, point_positions.as_slice()); - 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()); - + bind_new_buffer_to_attribute(&ctx, point_position_attr, + SPACE_DIM as i32, point_positions.as_slice()); + 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, + 1i32, scene.points.highlights.as_slice()); + bind_new_buffer_to_attribute(&ctx, point_selection_attr, + 1i32, scene.points.selections.as_slice()); + // draw the scene - ctx.draw_arrays(WebGl2RenderingContext::POINTS, 0, point_positions.ncols() as i32); - + 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 +841,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 +861,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 +880,7 @@ pub fn Display() -> View { event.prevent_default(); } }; - + view! { /* TO DO */ // switch back to integer-valued parameters when that becomes possible @@ -860,7 +902,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 +928,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()); @@ -915,7 +957,9 @@ pub fn Display() -> View { .into_iter() .filter(|elt| !elt.ghost().get()); for elt in tangible_elts { - match assembly_to_world.with(|asm_to_world| elt.cast(dir, asm_to_world, pixel_size)) { + let target = assembly_to_world.with( + |asm_to_world| elt.cast(dir, asm_to_world, pixel_size)); + match target { Some(depth) => match clicked { Some((_, best_depth)) => { if depth < best_depth { @@ -927,7 +971,7 @@ pub fn Display() -> View { None => (), }; } - + // if we clicked something, select it match clicked { Some((elt, _)) => state.select(&elt, event.shift_key()), @@ -936,4 +980,4 @@ pub fn Display() -> View { }, ) } -} \ No newline at end of file +} diff --git a/app-proto/src/components/outline.rs b/app-proto/src/components/outline.rs index 547b73b..9014b09 100644 --- a/app-proto/src/components/outline.rs +++ b/app-proto/src/components/outline.rs @@ -21,16 +21,16 @@ fn RegulatorInput(regulator: Rc) -> 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) -> 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", @@ -63,8 +63,10 @@ fn RegulatorInput(regulator: Rc) -> View { placeholder = measurement.with(|result| result.to_string()), bind:value = value, on:change = move |_| { + let specification = + SpecifiedValue::try_from(value.get_clone_untracked()); valid.set( - match SpecifiedValue::try_from(value.get_clone_untracked()) { + match specification { Ok(set_pt) => { set_point.set(set_pt); true @@ -141,7 +143,9 @@ fn ElementOutlineItem(element: Rc) -> View { let class = { let element_for_class = element.clone(); state.selection.map( - move |sel| if sel.contains(&element_for_class) { "selected" } else { "" } + move |sel| + if sel.contains(&element_for_class) { "selected" } + else { "" } ) }; let label = element.label().clone(); @@ -175,7 +179,8 @@ fn ElementOutlineItem(element: Rc) -> View { move |event: KeyboardEvent| { match event.key().as_str() { "Enter" => { - state.select(&element_for_handler, event.shift_key()); + state.select(&element_for_handler, + event.shift_key()); event.prevent_default(); }, "ArrowRight" if regulated.get() => { @@ -205,18 +210,22 @@ fn ElementOutlineItem(element: Rc) -> View { let state_for_handler = state.clone(); let element_for_handler = element.clone(); move |event: MouseEvent| { - state_for_handler.select(&element_for_handler, event.shift_key()); + state_for_handler.select(&element_for_handler, + event.shift_key()); event.stop_propagation(); event.prevent_default(); } } ) { div(class = "element-label") { (label) } - div(class = "element-representation") { (rep_components) } + div(class = "element-representation") { + (rep_components) + } input( r#type = "checkbox", bind:checked = element.ghost(), - on:click = |event: MouseEvent| event.stop_propagation() + on:click = + |event: MouseEvent| event.stop_propagation() ) } } @@ -241,7 +250,7 @@ fn ElementOutlineItem(element: Rc) -> View { #[component] pub fn Outline() -> View { let state = use_context::(); - + // list the elements alphabetically by ID /* TO DO */ // this code is designed to generalize easily to other sort keys. if we only @@ -254,7 +263,7 @@ pub fn Outline() -> View { .sorted_by_key(|elt| elt.id().clone()) .collect::>() ); - + view! { ul( id = "outline", @@ -272,4 +281,4 @@ pub fn Outline() -> View { ) } } -} \ No newline at end of file +} diff --git a/app-proto/src/components/point.frag b/app-proto/src/components/point.frag index 194a072..5e18bb4 100644 --- a/app-proto/src/components/point.frag +++ b/app-proto/src/components/point.frag @@ -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; -} \ No newline at end of file +} diff --git a/app-proto/src/components/point.vert b/app-proto/src/components/point.vert index 0b76bc1..14eb2e7 100644 --- a/app-proto/src/components/point.vert +++ b/app-proto/src/components/point.vert @@ -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; -} \ No newline at end of file +} diff --git a/app-proto/src/components/spheres.frag b/app-proto/src/components/spheres.frag index fa317a8..dc94cc0 100644 --- a/app-proto/src/components/spheres.frag +++ b/app-proto/src/components/spheres.frag @@ -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,15 @@ 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); - + 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 +187,7 @@ void main() { outColor = vec4(color, 1.); return; } - + // composite the sphere fragments vec3 color = vec3(0.); int layer = layer_cnt - 1; @@ -203,7 +204,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 +214,26 @@ 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)); + 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)); + 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.); -} \ No newline at end of file +} diff --git a/app-proto/src/components/test_assembly_chooser.rs b/app-proto/src/components/test_assembly_chooser.rs index 0d387d3..732a7e2 100644 --- a/app-proto/src/components/test_assembly_chooser.rs +++ b/app-proto/src/components/test_assembly_chooser.rs @@ -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( @@ -167,29 +167,36 @@ fn load_low_curvature(assembly: &Assembly) { let curvature = plane.regulators().with_untracked( |regs| regs.first().unwrap().clone() ); - curvature.set_point().set(SpecifiedValue::try_from("0".to_string()).unwrap()); + curvature.set_point().set( + SpecifiedValue::try_from("0".to_string()).unwrap()); } let all_perpendicular = [central.clone()].into_iter() .chain(sides.clone()) .chain(corners.clone()); for sphere in all_perpendicular { // make each side and packed sphere perpendicular to the assembly plane - let right_angle = InversiveDistanceRegulator::new([sphere, assemb_plane.clone()]); - right_angle.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap()); + let right_angle = InversiveDistanceRegulator::new( + [sphere, assemb_plane.clone()]); + right_angle.set_point.set( + SpecifiedValue::try_from("0".to_string()).unwrap()); assembly.insert_regulator(Rc::new(right_angle)); } for sphere in sides.clone().chain(corners.clone()) { // make each side and corner sphere tangent to the central sphere - let tangency = InversiveDistanceRegulator::new([sphere.clone(), central.clone()]); - tangency.set_point.set(SpecifiedValue::try_from("-1".to_string()).unwrap()); + let tangency = InversiveDistanceRegulator::new( + [sphere.clone(), central.clone()]); + tangency.set_point.set( + SpecifiedValue::try_from("-1".to_string()).unwrap()); assembly.insert_regulator(Rc::new(tangency)); } for (side_index, side) in sides.enumerate() { // make each side tangent to the two adjacent corner spheres for (corner_index, corner) in corners.clone().enumerate() { if side_index != corner_index { - let tangency = InversiveDistanceRegulator::new([side.clone(), corner]); - tangency.set_point.set(SpecifiedValue::try_from("-1".to_string()).unwrap()); + let tangency = InversiveDistanceRegulator::new( + [side.clone(), corner]); + tangency.set_point.set( + SpecifiedValue::try_from("-1".to_string()).unwrap()); assembly.insert_regulator(Rc::new(tangency)); } } @@ -217,21 +224,24 @@ 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 x32 = x as f32; + let y32 = y as f32; + let coords = + [0.5*(1.0 + x32), 0.5*(1.0 + y32), 0.5*(1.0 - x32*y32)]; let _ = assembly.try_insert_element( Sphere::new( format!("sphere{index_x}{index_y}"), format!("Sphere {index_x}{index_y}"), - [0.5*(1.0 + x) as f32, 0.5*(1.0 + y) as f32, 0.5*(1.0 - x*y) as f32], + coords, engine::sphere(x, y, 0.0, 1.0), ) ); - + let _ = assembly.try_insert_element( Point::new( format!("point{index_x}{index_y}"), format!("Point {index_x}{index_y}"), - [0.5*(1.0 + x) as f32, 0.5*(1.0 + y) as f32, 0.5*(1.0 - x*y) as f32], + coords, engine::point(x, y, 0.0), ) ); @@ -310,7 +320,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(); @@ -320,26 +330,32 @@ fn load_tridiminished_icosahedron(assembly: &Assembly) { "face1".to_string(), "Face 1".to_string(), COLOR_FACE, - engine::sphere_with_offset(frac_2_sqrt_6, -frac_1_sqrt_6, -frac_1_sqrt_6, -frac_1_sqrt_6, 0.0), + engine::sphere_with_offset( + frac_2_sqrt_6, -frac_1_sqrt_6, -frac_1_sqrt_6, + -frac_1_sqrt_6, 0.0), ), Sphere::new( "face2".to_string(), "Face 2".to_string(), COLOR_FACE, - engine::sphere_with_offset(-frac_1_sqrt_6, frac_2_sqrt_6, -frac_1_sqrt_6, -frac_1_sqrt_6, 0.0), + engine::sphere_with_offset( + -frac_1_sqrt_6, frac_2_sqrt_6, -frac_1_sqrt_6, + -frac_1_sqrt_6, 0.0), ), Sphere::new( "face3".to_string(), "Face 3".to_string(), COLOR_FACE, - engine::sphere_with_offset(-frac_1_sqrt_6, -frac_1_sqrt_6, frac_2_sqrt_6, -frac_1_sqrt_6, 0.0), + engine::sphere_with_offset( + -frac_1_sqrt_6, -frac_1_sqrt_6, frac_2_sqrt_6, + -frac_1_sqrt_6, 0.0), ), ]; for face in faces { 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,15 +368,17 @@ 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() ); - let incidence_a = InversiveDistanceRegulator::new([face.clone(), vertex_a.clone()]); - incidence_a.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap()); + 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,27 +388,30 @@ 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 let vertex = assembly.elements_by_id.with_untracked( |elts_by_id| elts_by_id[&format!("{series}{k}")].clone() ); - let incidence = InversiveDistanceRegulator::new([face.clone(), vertex.clone()]); - incidence.set_point.set(SpecifiedValue::try_from("0".to_string()).unwrap()); + 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])) + 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,19 +443,20 @@ 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]; const COLOR_C: ElementColor = [0.25_f32, 0.00_f32, 1.00_f32]; - let phi = 0.5 + 1.25_f64.sqrt(); /* TO DO */ // replace with std::f64::consts::PHI when that gets stabilized + /* TO DO */ // replace with std::f64::consts::PHI when that gets stabilized + let phi = 0.5 + 1.25_f64.sqrt(); let phi_inv = 1.0 / phi; let coord_scale = (phi + 2.0).sqrt(); let face_scales = [phi_inv, (13.0 / 12.0) / coord_scale]; @@ -445,10 +467,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 +486,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 +502,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,16 +520,19 @@ 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()); + 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 [long_edge_plane, short_edge_plane] + in [["a", "b"], ["b", "c"], ["c", "a"]] { for k in 0..2 { let long_edge_ids = [ format!("{long_edge_plane}{k}0"), @@ -524,14 +549,16 @@ fn load_dodecahedral_packing(assembly: &Assembly) { ) ) ); - + // set up the short-edge tangency - let short_tangency = InversiveDistanceRegulator::new(short_edge.clone()); + let short_tangency = InversiveDistanceRegulator::new( + short_edge.clone()); if k == 0 { - short_tangency.set_point.set(SpecifiedValue::try_from("-1".to_string()).unwrap()); + 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 { @@ -539,7 +566,9 @@ fn load_dodecahedral_packing(assembly: &Assembly) { [long_edge[i].clone(), short_edge[j].clone()] ); if i == 0 && k == 0 { - side_tangency.set_point.set(SpecifiedValue::try_from("-1".to_string()).unwrap()); + side_tangency.set_point.set( + SpecifiedValue::try_from("-1".to_string()).unwrap() + ); } assembly.insert_regulator(Rc::new(side_tangency)); } @@ -577,14 +606,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,12 +628,13 @@ 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] { let tangency = InversiveDistanceRegulator::new([outer.clone(), inner]); - tangency.set_point.set(SpecifiedValue::try_from("1".to_string()).unwrap()); + tangency.set_point.set( + SpecifiedValue::try_from("1".to_string()).unwrap()); assembly.insert_regulator(Rc::new(tangency)); } } @@ -629,14 +659,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 +680,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 +700,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 +729,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)); @@ -712,10 +742,14 @@ fn load_radius_ratio(assembly: &Assembly) { [0.25_f32, 0.00_f32, 1.00_f32], ].into_iter(), [ - engine::sphere_with_offset(base_dir[0], base_dir[1], base_dir[2], offset, 0.0), - engine::sphere_with_offset(base_dir[0], -base_dir[1], -base_dir[2], offset, 0.0), - engine::sphere_with_offset(-base_dir[0], base_dir[1], -base_dir[2], offset, 0.0), - engine::sphere_with_offset(-base_dir[0], -base_dir[1], base_dir[2], offset, 0.0), + engine::sphere_with_offset( + base_dir[0], base_dir[1], base_dir[2], offset, 0.0), + engine::sphere_with_offset( + base_dir[0], -base_dir[1], -base_dir[2], offset, 0.0), + engine::sphere_with_offset( + -base_dir[0], base_dir[1], -base_dir[2], offset, 0.0), + engine::sphere_with_offset( + -base_dir[0], -base_dir[1], base_dir[2], offset, 0.0), ].into_iter() ).map( |(k, color, representation)| { @@ -731,7 +765,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 +776,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 +784,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,10 +797,12 @@ 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()); + let incidence_regulator = InversiveDistanceRegulator::new( + [face_j.clone(), vertex_k.clone()]); + incidence_regulator.set_point.set( + SpecifiedValue::try_from("0".to_string()).unwrap()); assembly.insert_regulator(Rc::new(incidence_regulator)); } } @@ -799,7 +835,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 +872,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 +884,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( @@ -860,25 +896,33 @@ fn load_irisawa_hexlet(assembly: &Assembly) { |elts_by_id| elts_by_id[&format!("chain{k}")].clone() ) ); - for (chain_sphere, chain_sphere_next) in chain.clone().zip(chain.cycle().skip(1)) { + for (chain_sphere, chain_sphere_next) + in chain.clone().zip(chain.cycle().skip(1)) { for (other_sphere, inversive_distance) in [ (outer.clone(), "1"), (sun.clone(), "-1"), (moon.clone(), "-1"), (chain_sphere_next.clone(), "-1"), ] { - let tangency = InversiveDistanceRegulator::new([chain_sphere.clone(), other_sphere]); - tangency.set_point.set(SpecifiedValue::try_from(inversive_distance.to_string()).unwrap()); + let tangency = InversiveDistanceRegulator::new( + [chain_sphere.clone(), other_sphere]); + tangency.set_point.set( + SpecifiedValue::try_from( + inversive_distance.to_string()).unwrap()); 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()); + + 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()); + + 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,24 +939,25 @@ pub fn TestAssemblyChooser() -> View { console::log_1( &JsValue::from(format!("Showing assembly \"{}\"", name.clone())) ); - + batch(|| { let state = use_context::(); 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), "low-curvature" => load_low_curvature(assembly), "pointed" => load_pointed(assembly), - "tridiminished-icosahedron" => load_tridiminished_icosahedron(assembly), + "tridiminished-icosahedron" => + load_tridiminished_icosahedron(assembly), "dodecahedral-packing" => load_dodecahedral_packing(assembly), "balanced" => load_balanced(assembly), "off-center" => load_off_center(assembly), @@ -922,14 +967,16 @@ pub fn TestAssemblyChooser() -> View { }; }); }); - + // build the chooser view! { select(bind:value = assembly_name) { option(value = "general") { "General" } option(value = "low-curvature") { "Low-curvature" } option(value = "pointed") { "Pointed" } - option(value = "tridiminished-icosahedron") { "Tridiminished icosahedron" } + option(value = "tridiminished-icosahedron") { + "Tridiminished icosahedron" + } option(value = "dodecahedral-packing") { "Dodecahedral packing" } option(value = "balanced") { "Balanced" } option(value = "off-center") { "Off-center" } @@ -938,4 +985,4 @@ pub fn TestAssemblyChooser() -> View { option(value = "empty") { "Empty" } } } -} \ No newline at end of file +} diff --git a/app-proto/src/engine.rs b/app-proto/src/engine.rs index 0f26f02..a2a682f 100644 --- a/app-proto/src/engine.rs +++ b/app-proto/src/engine.rs @@ -9,8 +9,11 @@ pub fn point(x: f64, y: f64, z: f64) -> DVector { } // the sphere with the given center and radius, with inward-pointing normals -pub fn sphere(center_x: f64, center_y: f64, center_z: f64, radius: f64) -> DVector { - let center_norm_sq = center_x * center_x + center_y * center_y + center_z * center_z; +pub fn sphere(center_x: f64, center_y: f64, center_z: f64, radius: f64) + -> DVector +{ + let center_norm_sq = + center_x * center_x + center_y * center_y + center_z * center_z; DVector::from_column_slice(&[ center_x / radius, center_y / radius, @@ -23,7 +26,9 @@ pub fn sphere(center_x: f64, center_y: f64, center_z: f64, radius: f64) -> DVect // the sphere of curvature `curv` whose closest point to the origin has position // `off * dir` and normal `dir`, where `dir` is a unit vector. setting the // curvature to zero gives a plane -pub fn sphere_with_offset(dir_x: f64, dir_y: f64, dir_z: f64, off: f64, curv: f64) -> DVector { +pub fn sphere_with_offset( + dir_x: f64, dir_y: f64, dir_z: f64, off: f64, curv: f64) -> DVector +{ let norm_sp = 1.0 + off * curv; DVector::from_column_slice(&[ norm_sp * dir_x, @@ -62,19 +67,19 @@ impl PartialMatrix { pub fn new() -> Self { Self(Vec::::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) -> DMatrix { let mut result = a.clone(); for &MatrixEntry { index, value } in self { @@ -82,7 +87,7 @@ impl PartialMatrix { } result } - + fn proj(&self, a: &DMatrix) -> DMatrix { let mut result = DMatrix::::zeros(a.nrows(), a.ncols()); for &MatrixEntry { index, .. } in self { @@ -90,7 +95,7 @@ impl PartialMatrix { } result } - + fn sub_proj(&self, rhs: &DMatrix) -> DMatrix { let mut result = DMatrix::::zeros(rhs.nrows(), rhs.ncols()); for &MatrixEntry { index, value } in self { @@ -112,7 +117,7 @@ impl Display for PartialMatrix { impl IntoIterator for PartialMatrix { type Item = MatrixEntry; type IntoIter = std::vec::IntoIter; - + fn into_iter(self) -> Self::IntoIter { let Self(entries) = self; entries.into_iter() @@ -122,7 +127,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 +151,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 +172,10 @@ impl ConfigSubspace { |(λ, v)| (λ.abs() < THRESHOLD).then_some(v) ).collect::>().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,20 +192,22 @@ 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 // projection inner product - pub fn proj(&self, v: &DVectorView, column_index: usize) -> DMatrix { + pub fn proj(&self, v: &DVectorView, column_index: usize) + -> DMatrix + { if self.dim() == 0 { const ELEMENT_DIM: usize = 5; DMatrix::zeros(ELEMENT_DIM, self.assembly_dim) @@ -253,7 +260,7 @@ impl ConstraintProblem { guess: DMatrix::::zeros(ELEMENT_DIM, element_count), } } - + #[cfg(feature = "dev")] pub fn from_guess(guess_columns: &[DVector]) -> Self { Self { @@ -291,7 +298,9 @@ impl SearchState { } } -fn basis_matrix(index: (usize, usize), nrows: usize, ncols: usize) -> DMatrix { +fn basis_matrix(index: (usize, usize), nrows: usize, ncols: usize) + -> DMatrix +{ let mut result = DMatrix::::zeros(nrows, ncols); result[index] = 1.0; result @@ -377,10 +386,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,29 +403,30 @@ 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 = 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); for _ in 0..max_descent_steps { // find the negative gradient of the loss function 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>); + 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::>::with_capacity(total_dim); for col in 0..assembly_dim { @@ -431,19 +441,21 @@ pub fn realize_gram( -&basis_mat * &state.err_proj + &state.config * &neg_d_err_proj ); - hess_cols.push(deriv_grad.reshape_generic(Dyn(total_dim), Const::<1>)); + hess_cols.push( + deriv_grad.reshape_generic(Dyn(total_dim), Const::<1>)); } } hess = DMatrix::from_columns(hess_cols.as_slice()); - + // regularize the Hessian let hess_eigvals = hess.symmetric_eigenvalues(); let min_eigval = hess_eigvals.min(); if min_eigval <= 0.0 { - hess -= reg_scale * min_eigval * DMatrix::identity(total_dim, total_dim); + 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 +466,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 */ /* @@ -477,9 +489,10 @@ 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)); + 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,11 +518,14 @@ 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 }) + 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 +537,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 +563,40 @@ pub mod examples { ) ).collect::>().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,27 +617,28 @@ pub mod examples { } ).collect::>().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; for j in 0..2 { // diagonal and hinge edges for k in j..2 { - problem.gram.push_sym(block + j, block + k, if j == k { 0.0 } else { -0.5 }); + 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 +647,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 +668,7 @@ mod tests { ]); assert_eq!(frozen.freeze(&config), expected_result); } - + #[test] fn sub_proj_test() { let target = PartialMatrix(vec![ @@ -670,7 +687,7 @@ mod tests { ]); assert_eq!(target.sub_proj(&attempt), expected_result); } - + #[test] fn zero_loss_test() { let mut gram = PartialMatrix::new(); @@ -690,7 +707,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 @@ -702,7 +719,8 @@ mod tests { ]); for j in 0..2 { for k in j..2 { - problem.gram.push_sym(j, k, if (j, k) == (1, 1) { 1.0 } else { 0.0 }); + problem.gram.push_sym( + j, k, if (j, k) == (1, 1) { 1.0 } else { 0.0 }); } } problem.frozen.push(3, 0, problem.guess[(3, 0)]); @@ -720,21 +738,22 @@ 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]; + 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]; for (k, diam) in solution_diams.into_iter().enumerate() { 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 +777,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,30 +805,37 @@ 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 - let tol_sq = ((element_dim * assembly_dim) as f64) * SCALED_TOL * SCALED_TOL; - for (motion_unif, motion_std) in tangent_motions_unif.into_iter().zip(tangent_motions_std) { - let motion_proj: DMatrix<_> = motion_unif.column_iter().enumerate().map( - |(k, v)| tangent.proj(&v, k) - ).sum(); + let tol_sq = ((element_dim * assembly_dim) as f64) + * SCALED_TOL * SCALED_TOL; + for (motion_unif, motion_std) + in tangent_motions_unif.into_iter().zip(tangent_motions_std) { + let motion_proj: DMatrix<_> = + motion_unif.column_iter().enumerate().map( + |(k, v)| tangent.proj(&v, k) + ).sum(); assert!((motion_std - motion_proj).norm_squared() < tol_sq); } } - - fn translation_motion_unif(vel: &Vector3, assembly_dim: usize) -> Vec> { + + fn translation_motion_unif(vel: &Vector3, assembly_dim: usize) + -> Vec> + { 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, points: Vec>) -> Vec> { + + fn rotation_motion_unif( + ang_vel: &Vector3, points: Vec> + ) -> Vec> { points.into_iter().map( |pt| { let vel = ang_vel.cross(&pt.fixed_rows::<3>(0)); @@ -819,7 +845,7 @@ mod tests { } ).collect() } - + #[test] fn tangent_test_kaleidocycle() { // set up a kaleidocycle and find its tangent space @@ -827,7 +853,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 +864,15 @@ 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()), - + 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: @@ -859,8 +888,10 @@ mod tests { [ DVector::from_column_slice(&[0.0, 0.0, 5.0, 0.0]), DVector::from_column_slice(&[0.0, 0.0, 1.0, 0.0]), - DVector::from_column_slice(&[-vel_vert_x, -vel_vert_y, -3.0, 0.0]), - DVector::from_column_slice(&[vel_vert_x, vel_vert_y, -3.0, 0.0]), + DVector::from_column_slice( + &[-vel_vert_x, -vel_vert_y, -3.0, 0.0]), + DVector::from_column_slice( + &[vel_vert_x, vel_vert_y, -3.0, 0.0]), ] } ).collect::>(), @@ -872,23 +903,26 @@ mod tests { ).collect::>() ) ).collect::>(); - + // 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 - let tol_sq = ((element_dim * assembly_dim) as f64) * SCALED_TOL * SCALED_TOL; - for (motion_unif, motion_std) in tangent_motions_unif.into_iter().zip(tangent_motions_std) { - let motion_proj: DMatrix<_> = motion_unif.into_iter().enumerate().map( - |(k, v)| tangent.proj(&v.as_view(), k) - ).sum(); + let tol_sq = ((element_dim * assembly_dim) as f64) + * SCALED_TOL * SCALED_TOL; + for (motion_unif, motion_std) + in tangent_motions_unif.into_iter().zip(tangent_motions_std) { + let motion_proj: DMatrix<_> = + motion_unif.into_iter().enumerate().map( + |(k, v)| tangent.proj(&v.as_view(), k) + ).sum(); assert!((motion_std - motion_proj).norm_squared() < tol_sq); } } - + fn translation(dis: Vector3) -> DMatrix { const ELEMENT_DIM: usize = 5; DMatrix::from_column_slice(ELEMENT_DIM, ELEMENT_DIM, &[ @@ -899,7 +933,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] @@ -913,13 +947,13 @@ mod tests { problem_orig.gram.push_sym(0, 0, 1.0); problem_orig.gram.push_sym(1, 1, 1.0); problem_orig.gram.push_sym(0, 1, 0.5); - let Realization { result: result_orig, history: history_orig } = realize_gram( - &problem_orig, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 - ); - let ConfigNeighborhood { config: config_orig, nbhd: tangent_orig } = result_orig.unwrap(); + let Realization { result: result_orig, history: history_orig } = + realize_gram(&problem_orig, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110); + 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 = { @@ -934,23 +968,24 @@ mod tests { frozen: problem_orig.frozen, guess: guess_tfm, }; - let Realization { result: result_tfm, history: history_tfm } = realize_gram( - &problem_tfm, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110 - ); - let ConfigNeighborhood { config: config_tfm, nbhd: tangent_tfm } = result_tfm.unwrap(); + let Realization { result: result_tfm, history: history_tfm } = + realize_gram(&problem_tfm, SCALED_TOL, 0.5, 0.9, 1.1, 200, 110); + 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 = 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,12 +999,14 @@ 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 const SCALED_TOL_TFM: f64 = 1.0e-9; - let tol_sq = ((problem_orig.guess.nrows() * problem_orig.guess.ncols()) as f64) * SCALED_TOL_TFM * SCALED_TOL_TFM; + 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); } -} \ No newline at end of file +} diff --git a/app-proto/src/main.rs b/app-proto/src/main.rs index a03b026..d8fb030 100644 --- a/app-proto/src/main.rs +++ b/app-proto/src/main.rs @@ -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, 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 {} } }); -} \ No newline at end of file +} diff --git a/app-proto/src/specified.rs b/app-proto/src/specified.rs index b0f04b5..d54e75c 100644 --- a/app-proto/src/specified.rs +++ b/app-proto/src/specified.rs @@ -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> for SpecifiedValue { // if the specification is properly formatted, and `Error` if not impl TryFrom for SpecifiedValue { type Error = ParseFloatError; - + fn try_from(spec: String) -> Result { if spec.is_empty() { Ok(Self::from_empty_spec()) @@ -52,4 +52,4 @@ impl TryFrom for SpecifiedValue { ) } } -} \ No newline at end of file +}