chore: remove trailing whitespace, add CR at end of file

This commit is contained in:
Glen Whitney 2025-10-10 10:20:38 -07:00
parent a4b355d943
commit 3635abc562
11 changed files with 320 additions and 320 deletions

View file

@ -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;

View file

@ -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 {} }
}
}
}
}

View file

@ -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 {
},
)
}
}
}

View file

@ -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 {
)
}
}
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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.);
}
}

View file

@ -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" }
}
}
}
}

View file

@ -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);
}
}
}

View file

@ -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 {}
}
});
}
}

View file

@ -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 {
)
}
}
}
}