From 91e4e1f4143a1632621ccce498ceb8f0e7d41d18 Mon Sep 17 00:00:00 2001 From: Aaron Fenyes Date: Mon, 21 Jul 2025 12:16:16 -0700 Subject: [PATCH] Make the test assembly chooser its own component --- app-proto/src/add_remove.rs | 937 +----------------------- app-proto/src/main.rs | 1 + app-proto/src/test_assembly_chooser.rs | 947 +++++++++++++++++++++++++ 3 files changed, 952 insertions(+), 933 deletions(-) create mode 100644 app-proto/src/test_assembly_chooser.rs diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs index fbe37c2..de9272e 100644 --- a/app-proto/src/add_remove.rs +++ b/app-proto/src/add_remove.rs @@ -1,932 +1,14 @@ -use itertools::izip; -use std::{f64::consts::{FRAC_1_SQRT_2, PI}, rc::Rc}; -use nalgebra::Vector3; +use std::rc::Rc; use sycamore::prelude::*; -use web_sys::{console, wasm_bindgen::JsValue}; use crate::{ AppState, - engine, - engine::DescentHistory, - assembly::{ - Assembly, - Element, - ElementColor, - InversiveDistanceRegulator, - Point, - Sphere - }, - specified::SpecifiedValue + assembly::{InversiveDistanceRegulator, Point, Sphere}, + test_assembly_chooser::TestAssemblyChooser }; -/* DEBUG */ -// load an example assembly for testing. this code will be removed once we've -// built a more formal test assembly system -fn load_gen_assemb(assembly: &Assembly) { - let _ = assembly.try_insert_element( - Sphere::new( - String::from("gemini_a"), - String::from("Castor"), - [1.00_f32, 0.25_f32, 0.00_f32], - engine::sphere(0.5, 0.5, 0.0, 1.0) - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - String::from("gemini_b"), - String::from("Pollux"), - [0.00_f32, 0.25_f32, 1.00_f32], - engine::sphere(-0.5, -0.5, 0.0, 1.0) - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - String::from("ursa_major"), - String::from("Ursa major"), - [0.25_f32, 0.00_f32, 1.00_f32], - engine::sphere(-0.5, 0.5, 0.0, 0.75) - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - String::from("ursa_minor"), - String::from("Ursa minor"), - [0.25_f32, 1.00_f32, 0.00_f32], - engine::sphere(0.5, -0.5, 0.0, 0.5) - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - String::from("moon_deimos"), - String::from("Deimos"), - [0.75_f32, 0.75_f32, 0.00_f32], - engine::sphere(0.0, 0.15, 1.0, 0.25) - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - String::from("moon_phobos"), - String::from("Phobos"), - [0.00_f32, 0.75_f32, 0.50_f32], - engine::sphere(0.0, -0.15, -1.0, 0.25) - ) - ); -} - -/* DEBUG */ -// load an example assembly for testing. this code will be removed once we've -// built a more formal test assembly system -fn load_low_curv_assemb(assembly: &Assembly) { - // create the spheres - let a = 0.75_f64.sqrt(); - let _ = assembly.try_insert_element( - Sphere::new( - "central".to_string(), - "Central".to_string(), - [0.75_f32, 0.75_f32, 0.75_f32], - engine::sphere(0.0, 0.0, 0.0, 1.0) - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - "assemb_plane".to_string(), - "Assembly plane".to_string(), - [0.75_f32, 0.75_f32, 0.75_f32], - engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0) - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - "side1".to_string(), - "Side 1".to_string(), - [1.00_f32, 0.00_f32, 0.25_f32], - engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0) - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - "side2".to_string(), - "Side 2".to_string(), - [0.25_f32, 1.00_f32, 0.00_f32], - engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0) - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - "side3".to_string(), - "Side 3".to_string(), - [0.00_f32, 0.25_f32, 1.00_f32], - engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0) - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - "corner1".to_string(), - "Corner 1".to_string(), - [0.75_f32, 0.75_f32, 0.75_f32], - engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0) - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - "corner2".to_string(), - "Corner 2".to_string(), - [0.75_f32, 0.75_f32, 0.75_f32], - engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0) - ) - ); - let _ = assembly.try_insert_element( - Sphere::new( - String::from("corner3"), - String::from("Corner 3"), - [0.75_f32, 0.75_f32, 0.75_f32], - 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( - |id| assembly.elements_by_id.with_untracked( - |elts_by_id| elts_by_id[id].clone() - ) - ); - let sides = index_range.clone().map( - |k| assembly.elements_by_id.with_untracked( - |elts_by_id| elts_by_id[&format!("side{k}")].clone() - ) - ); - let corners = index_range.map( - |k| assembly.elements_by_id.with_untracked( - |elts_by_id| elts_by_id[&format!("corner{k}")].clone() - ) - ); - for plane in [assemb_plane.clone()].into_iter().chain(sides.clone()) { - // fix the curvature of each plane - let curvature = plane.regulators().with_untracked( - |regs| regs.first().unwrap().clone() - ); - 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()); - 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()); - 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()); - assembly.insert_regulator(Rc::new(tangency)); - } - } - } -} - -fn load_pointed_assemb(assembly: &Assembly) { - let _ = assembly.try_insert_element( - Point::new( - format!("point_front"), - format!("Front point"), - [0.75_f32, 0.75_f32, 0.75_f32], - engine::point(0.0, 0.0, FRAC_1_SQRT_2) - ) - ); - let _ = assembly.try_insert_element( - Point::new( - format!("point_back"), - format!("Back point"), - [0.75_f32, 0.75_f32, 0.75_f32], - engine::point(0.0, 0.0, -FRAC_1_SQRT_2) - ) - ); - for index_x in 0..=1 { - 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}"), - 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], - 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], - engine::point(x, y, 0.0) - ) - ); - } - } -} - -// to finish describing the tridiminished icosahedron, set the inversive -// distance regulators as follows: -// A-A -0.25 -// A-B " -// B-C " -// C-C " -// A-C -0.25 * φ^2 = -0.6545084971874737 -fn load_tridim_icosahedron_assemb(assembly: &Assembly) { - // create the vertices - const COLOR_A: ElementColor = [1.00_f32, 0.25_f32, 0.25_f32]; - const COLOR_B: ElementColor = [0.75_f32, 0.75_f32, 0.75_f32]; - const COLOR_C: ElementColor = [0.25_f32, 0.50_f32, 1.00_f32]; - let vertices = [ - Point::new( - "a1".to_string(), - "A₁".to_string(), - COLOR_A, - engine::point(0.25, 0.75, 0.75) - ), - Point::new( - "a2".to_string(), - "A₂".to_string(), - COLOR_A, - engine::point(0.75, 0.25, 0.75) - ), - Point::new( - "a3".to_string(), - "A₃".to_string(), - COLOR_A, - engine::point(0.75, 0.75, 0.25) - ), - Point::new( - "b1".to_string(), - "B₁".to_string(), - COLOR_B, - engine::point(0.75, -0.25, -0.25) - ), - Point::new( - "b2".to_string(), - "B₂".to_string(), - COLOR_B, - engine::point(-0.25, 0.75, -0.25) - ), - Point::new( - "b3".to_string(), - "B₃".to_string(), - COLOR_B, - engine::point(-0.25, -0.25, 0.75) - ), - Point::new( - "c1".to_string(), - "C₁".to_string(), - COLOR_C, - engine::point(0.0, -1.0, -1.0) - ), - Point::new( - "c2".to_string(), - "C₂".to_string(), - COLOR_C, - engine::point(-1.0, 0.0, -1.0) - ), - Point::new( - "c3".to_string(), - "C₃".to_string(), - COLOR_C, - engine::point(-1.0, -1.0, 0.0) - ) - ]; - 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(); - let frac_2_sqrt_6 = 2.0 * frac_1_sqrt_6; - let faces = [ - Sphere::new( - "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) - ), - 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) - ), - 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) - ) - ]; - 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 - let face = assembly.elements_by_id.with_untracked( - |elts_by_id| elts_by_id[&format!("face{j}")].clone() - ); - let curvature_regulator = face.regulators().with_untracked( - |regs| regs.first().unwrap().clone() - ); - 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()); - 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( - |elts_by_id| elts_by_id[&format!("{series}{j}")].clone() - ) - ); - 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()); - 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( - |index| assembly.elements_by_id.with_untracked( - |elts_by_id| elts_by_id[&format!("{series}{index}")].clone() - ) - ) - ); - for pair in adjacent_pairs { - assembly.insert_regulator( - Rc::new(InversiveDistanceRegulator::new(pair)) - ); - } - } -} - -// to finish describing the dodecahedral circle packing, set the inversive -// distance regulators to -1. some of the regulators have already been set -fn load_dodeca_packing_assemb(assembly: &Assembly) { - // add the substrate - let _ = assembly.try_insert_element( - Sphere::new( - "substrate".to_string(), - "Substrate".to_string(), - [0.75_f32, 0.75_f32, 0.75_f32], - engine::sphere(0.0, 0.0, 0.0, 1.0) - ) - ); - 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 - let phi_inv = 1.0 / phi; - let coord_scale = (phi + 2.0).sqrt(); - let face_scales = [phi_inv, (13.0 / 12.0) / coord_scale]; - let face_radii = [phi_inv, 5.0 / 12.0]; - let mut faces = Vec::>::new(); - let subscripts = ["₀", "₁"]; - for j in 0..2 { - 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( - Sphere::new( - id_a.clone(), - format!("A{label_sub}"), - COLOR_A, - engine::sphere(0.0, small_coord, big_coord, face_radii[k]) - ) - ); - faces.push( - assembly.elements_by_id.with_untracked( - |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( - Sphere::new( - id_b.clone(), - format!("B{label_sub}"), - COLOR_B, - engine::sphere(small_coord, big_coord, 0.0, face_radii[k]) - ) - ); - faces.push( - assembly.elements_by_id.with_untracked( - |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( - Sphere::new( - id_c.clone(), - format!("C{label_sub}"), - COLOR_C, - engine::sphere(big_coord, 0.0, small_coord, face_radii[k]) - ) - ); - faces.push( - assembly.elements_by_id.with_untracked( - |elts_by_id| elts_by_id[&id_c].clone() - ) - ); - } - } - - // 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 { - let long_edge_ids = [ - format!("{long_edge_plane}{k}0"), - format!("{long_edge_plane}{k}1") - ]; - let short_edge_ids = [ - format!("{short_edge_plane}0{k}"), - format!("{short_edge_plane}1{k}") - ]; - let [long_edge, short_edge] = [long_edge_ids, short_edge_ids].map( - |edge_ids| edge_ids.map( - |id| assembly.elements_by_id.with_untracked( - |elts_by_id| elts_by_id[&id].clone() - ) - ) - ); - - // 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 { - let side_tangency = InversiveDistanceRegulator::new( - [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()); - } - assembly.insert_regulator(Rc::new(side_tangency)); - } - } - } - } -} - -/* DEBUG */ -// the initial configuration of this test assembly deliberately violates the -// constraints, so loading the assembly will trigger a non-trivial realization -fn load_balanced_assemb(assembly: &Assembly) { - // create the spheres - const R_OUTER: f64 = 10.0; - const R_INNER: f64 = 4.0; - let spheres = [ - Sphere::new( - "outer".to_string(), - "Outer".to_string(), - [0.75_f32, 0.75_f32, 0.75_f32], - engine::sphere(0.0, 0.0, 0.0, R_OUTER) - ), - Sphere::new( - "a".to_string(), - "A".to_string(), - [1.00_f32, 0.00_f32, 0.25_f32], - engine::sphere(0.0, 4.0, 0.0, R_INNER) - ), - Sphere::new( - "b".to_string(), - "B".to_string(), - [0.00_f32, 0.25_f32, 1.00_f32], - engine::sphere(0.0, -4.0, 0.0, R_INNER) - ), - ]; - 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), - (a.clone(), R_INNER), - (b.clone(), R_INNER) - ] { - let curvature_regulator = sphere.regulators().with_untracked( - |regs| regs.first().unwrap().clone() - ); - let curvature = 0.5 / radius; - curvature_regulator.set_point().set( - 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()); - assembly.insert_regulator(Rc::new(tangency)); - } -} - -/* DEBUG */ -// the initial configuration of this test assembly deliberately violates the -// constraints, so loading the assembly will trigger a non-trivial realization -fn load_off_center_assemb(assembly: &Assembly) { - // create a point almost at the origin and a sphere centered on the origin - let _ = assembly.try_insert_element( - Point::new( - "point".to_string(), - "Point".to_string(), - [0.75_f32, 0.75_f32, 0.75_f32], - engine::point(1e-9, 0.0, 0.0) - ), - ); - let _ = assembly.try_insert_element( - Sphere::new( - "sphere".to_string(), - "Sphere".to_string(), - [0.75_f32, 0.75_f32, 0.75_f32], - 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()); - assembly.insert_regulator(Rc::new(incidence)); -} - -// setting the inversive distances between the vertices to -2 gives a regular -// tetrahedron with side length 1, whose insphere and circumsphere have radii -// sqrt(1/6) and sqrt(3/2), respectively. to measure those radii, set an -// inversive distance of -1 between the insphere and each face, and then set an -// inversive distance of 0 between the circumsphere and each vertex -fn load_radius_ratio_assemb(assembly: &Assembly) { - let index_range = 1..=4; - - // create the spheres - const GRAY: ElementColor = [0.75_f32, 0.75_f32, 0.75_f32]; - let spheres = [ - Sphere::new( - "sphere_faces".to_string(), - "Insphere".to_string(), - GRAY, - engine::sphere(0.0, 0.0, 0.0, 0.5) - ), - Sphere::new( - "sphere_vertices".to_string(), - "Circumsphere".to_string(), - GRAY, - engine::sphere(0.0, 0.0, 0.0, 0.25) - ) - ]; - for sphere in spheres { - let _ = assembly.try_insert_element(sphere); - } - - // create the vertices - let vertices = izip!( - index_range.clone(), - [ - [1.00_f32, 0.50_f32, 0.75_f32], - [1.00_f32, 0.75_f32, 0.50_f32], - [1.00_f32, 1.00_f32, 0.50_f32], - [0.75_f32, 0.50_f32, 1.00_f32] - ].into_iter(), - [ - engine::point(-0.6, -0.8, -0.6), - engine::point(-0.6, 0.8, 0.6), - engine::point(0.6, -0.8, 0.6), - engine::point(0.6, 0.8, -0.6) - ].into_iter() - ).map( - |(k, color, representation)| { - Point::new( - format!("v{k}"), - format!("Vertex {k}"), - color, - representation - ) - } - ); - 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)); - let faces = izip!( - index_range.clone(), - [ - [1.00_f32, 0.00_f32, 0.25_f32], - [1.00_f32, 0.25_f32, 0.00_f32], - [0.75_f32, 0.75_f32, 0.00_f32], - [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) - ].into_iter() - ).map( - |(k, color, representation)| { - Sphere::new( - format!("f{k}"), - format!("Face {k}"), - color, - representation - ) - } - ); - for face in faces { - face.ghost().set(true); - let _ = assembly.try_insert_element(face); - } - - // impose the constraints - for j in index_range.clone() { - let [face_j, vertex_j] = [ - format!("f{j}"), - format!("v{j}") - ].map( - |id| assembly.elements_by_id.with_untracked( - |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() - ); - 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( - [vertex_j.clone(), vertex_k.clone()] - ); - 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()); - assembly.insert_regulator(Rc::new(incidence_regulator)); - } - } -} - -// to finish setting up the problem, fix the following curvatures: -// sun 1 -// moon 5/3 = 1.666666666666666... -// chain1 2 -// a tiny `x` or `z` nudge of the outer sphere reliably prevents realization -// failures before they happen, or resolves them after they happen. the result -// depends sensitively on the translation direction, suggesting that realization -// is failing because the engine is having trouble breaking a symmetry -// /* TO DO */ -// the engine's performance on this problem is scale-dependent! with the current -// initial conditions, realization fails for any order of imposing the remaining -// curvature constraints. scaling everything up by a factor of ten, as done in -// the original problem, makes realization succeed reliably. one potentially -// relevant difference is that a lot of the numbers in the current initial -// conditions are exactly representable as floats, unlike the analogous numbers -// in the scaled-up problem. the inexact representations might break the -// symmetry that's getting the engine stuck -fn load_irisawa_hexlet_assemb(assembly: &Assembly) { - let index_range = 1..=6; - let colors = [ - [1.00_f32, 0.00_f32, 0.25_f32], - [1.00_f32, 0.25_f32, 0.00_f32], - [0.75_f32, 0.75_f32, 0.00_f32], - [0.25_f32, 1.00_f32, 0.00_f32], - [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( - "outer".to_string(), - "Outer".to_string(), - [0.5_f32, 0.5_f32, 0.5_f32], - engine::sphere(0.0, 0.0, 0.0, 1.5) - ), - Sphere::new( - "sun".to_string(), - "Sun".to_string(), - [0.75_f32, 0.75_f32, 0.75_f32], - engine::sphere(0.0, -0.75, 0.0, 0.75) - ), - Sphere::new( - "moon".to_string(), - "Moon".to_string(), - [0.25_f32, 0.25_f32, 0.25_f32], - engine::sphere(0.0, 0.75, 0.0, 0.75) - ), - ].into_iter().chain( - index_range.clone().zip(colors).map( - |(k, color)| { - let ang = (k as f64) * PI/3.0; - Sphere::new( - format!("chain{k}"), - format!("Chain {k}"), - color, - engine::sphere(1.0 * ang.sin(), 0.0, 1.0 * ang.cos(), 0.5) - ) - } - ) - ); - 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() - ); - outer.ghost().set(true); - let outer_curvature_regulator = outer.regulators().with_untracked( - |regs| regs.first().unwrap().clone() - ); - 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( - |elts_by_id| elts_by_id[id].clone() - ) - ); - let chain = index_range.map( - |k| assembly.elements_by_id.with_untracked( - |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 (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()); - 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)); -} - #[component] pub fn AddRemove() -> View { - /* DEBUG */ - let assembly_name = create_signal("general".to_string()); - create_effect(move || { - // get name of chosen assembly - let name = assembly_name.get_clone(); - console::log_1( - &JsValue::from(format!("Showing assembly \"{}\"", name.clone())) - ); - - batch(|| { - let state = use_context::(); - let assembly = &state.assembly; - - // pause realization - assembly.keep_realized.set(false); - - // 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_gen_assemb(assembly), - "low-curv" => load_low_curv_assemb(assembly), - "pointed" => load_pointed_assemb(assembly), - "tridim-icosahedron" => load_tridim_icosahedron_assemb(assembly), - "dodeca-packing" => load_dodeca_packing_assemb(assembly), - "balanced" => load_balanced_assemb(assembly), - "off-center" => load_off_center_assemb(assembly), - "radius-ratio" => load_radius_ratio_assemb(assembly), - "irisawa-hexlet" => load_irisawa_hexlet_assemb(assembly), - _ => () - }; - - // resume realization - assembly.keep_realized.set(true); - }); - }); - view! { div(id="add-remove") { button( @@ -966,18 +48,7 @@ pub fn AddRemove() -> View { state.selection.update(|sel| sel.clear()); } ) { "🔗" } - select(bind:value=assembly_name) { /* DEBUG */ // example assembly chooser - option(value="general") { "General" } - option(value="low-curv") { "Low-curvature" } - option(value="pointed") { "Pointed" } - option(value="tridim-icosahedron") { "Tridiminished icosahedron" } - option(value="dodeca-packing") { "Dodecahedral packing" } - option(value="balanced") { "Balanced" } - option(value="off-center") { "Off-center" } - option(value="radius-ratio") { "Radius ratio" } - option(value="irisawa-hexlet") { "Irisawa hexlet" } - option(value="empty") { "Empty" } - } + TestAssemblyChooser {} } } } \ No newline at end of file diff --git a/app-proto/src/main.rs b/app-proto/src/main.rs index f905c46..f5b7cef 100644 --- a/app-proto/src/main.rs +++ b/app-proto/src/main.rs @@ -5,6 +5,7 @@ mod display; mod engine; mod outline; mod specified; +mod test_assembly_chooser; #[cfg(test)] mod tests; diff --git a/app-proto/src/test_assembly_chooser.rs b/app-proto/src/test_assembly_chooser.rs new file mode 100644 index 0000000..232cda3 --- /dev/null +++ b/app-proto/src/test_assembly_chooser.rs @@ -0,0 +1,947 @@ +use itertools::izip; +use std::{f64::consts::{FRAC_1_SQRT_2, PI}, rc::Rc}; +use nalgebra::Vector3; +use sycamore::prelude::*; +use web_sys::{console, wasm_bindgen::JsValue}; + +use crate::{ + AppState, + engine, + engine::DescentHistory, + assembly::{ + Assembly, + Element, + ElementColor, + InversiveDistanceRegulator, + Point, + Sphere + }, + specified::SpecifiedValue +}; + +// --- loaders --- + +/* DEBUG */ +// each of these functions loads an example assembly for testing. once we've +// done more work on saving and loading assemblies, we should come back to this +// code to see if it can be simplified + +fn load_gen_assemb(assembly: &Assembly) { + let _ = assembly.try_insert_element( + Sphere::new( + String::from("gemini_a"), + String::from("Castor"), + [1.00_f32, 0.25_f32, 0.00_f32], + engine::sphere(0.5, 0.5, 0.0, 1.0) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + String::from("gemini_b"), + String::from("Pollux"), + [0.00_f32, 0.25_f32, 1.00_f32], + engine::sphere(-0.5, -0.5, 0.0, 1.0) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + String::from("ursa_major"), + String::from("Ursa major"), + [0.25_f32, 0.00_f32, 1.00_f32], + engine::sphere(-0.5, 0.5, 0.0, 0.75) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + String::from("ursa_minor"), + String::from("Ursa minor"), + [0.25_f32, 1.00_f32, 0.00_f32], + engine::sphere(0.5, -0.5, 0.0, 0.5) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + String::from("moon_deimos"), + String::from("Deimos"), + [0.75_f32, 0.75_f32, 0.00_f32], + engine::sphere(0.0, 0.15, 1.0, 0.25) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + String::from("moon_phobos"), + String::from("Phobos"), + [0.00_f32, 0.75_f32, 0.50_f32], + engine::sphere(0.0, -0.15, -1.0, 0.25) + ) + ); +} + +fn load_low_curv_assemb(assembly: &Assembly) { + // create the spheres + let a = 0.75_f64.sqrt(); + let _ = assembly.try_insert_element( + Sphere::new( + "central".to_string(), + "Central".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(0.0, 0.0, 0.0, 1.0) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + "assemb_plane".to_string(), + "Assembly plane".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + "side1".to_string(), + "Side 1".to_string(), + [1.00_f32, 0.00_f32, 0.25_f32], + engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + "side2".to_string(), + "Side 2".to_string(), + [0.25_f32, 1.00_f32, 0.00_f32], + engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + "side3".to_string(), + "Side 3".to_string(), + [0.00_f32, 0.25_f32, 1.00_f32], + engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + "corner1".to_string(), + "Corner 1".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + "corner2".to_string(), + "Corner 2".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0) + ) + ); + let _ = assembly.try_insert_element( + Sphere::new( + String::from("corner3"), + String::from("Corner 3"), + [0.75_f32, 0.75_f32, 0.75_f32], + 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( + |id| assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[id].clone() + ) + ); + let sides = index_range.clone().map( + |k| assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[&format!("side{k}")].clone() + ) + ); + let corners = index_range.map( + |k| assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[&format!("corner{k}")].clone() + ) + ); + for plane in [assemb_plane.clone()].into_iter().chain(sides.clone()) { + // fix the curvature of each plane + let curvature = plane.regulators().with_untracked( + |regs| regs.first().unwrap().clone() + ); + 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()); + 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()); + 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()); + assembly.insert_regulator(Rc::new(tangency)); + } + } + } +} + +fn load_pointed_assemb(assembly: &Assembly) { + let _ = assembly.try_insert_element( + Point::new( + format!("point_front"), + format!("Front point"), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::point(0.0, 0.0, FRAC_1_SQRT_2) + ) + ); + let _ = assembly.try_insert_element( + Point::new( + format!("point_back"), + format!("Back point"), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::point(0.0, 0.0, -FRAC_1_SQRT_2) + ) + ); + for index_x in 0..=1 { + 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}"), + 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], + 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], + engine::point(x, y, 0.0) + ) + ); + } + } +} + +// to finish describing the tridiminished icosahedron, set the inversive +// distance regulators as follows: +// A-A -0.25 +// A-B " +// B-C " +// C-C " +// A-C -0.25 * φ^2 = -0.6545084971874737 +fn load_tridim_icosahedron_assemb(assembly: &Assembly) { + // create the vertices + const COLOR_A: ElementColor = [1.00_f32, 0.25_f32, 0.25_f32]; + const COLOR_B: ElementColor = [0.75_f32, 0.75_f32, 0.75_f32]; + const COLOR_C: ElementColor = [0.25_f32, 0.50_f32, 1.00_f32]; + let vertices = [ + Point::new( + "a1".to_string(), + "A₁".to_string(), + COLOR_A, + engine::point(0.25, 0.75, 0.75) + ), + Point::new( + "a2".to_string(), + "A₂".to_string(), + COLOR_A, + engine::point(0.75, 0.25, 0.75) + ), + Point::new( + "a3".to_string(), + "A₃".to_string(), + COLOR_A, + engine::point(0.75, 0.75, 0.25) + ), + Point::new( + "b1".to_string(), + "B₁".to_string(), + COLOR_B, + engine::point(0.75, -0.25, -0.25) + ), + Point::new( + "b2".to_string(), + "B₂".to_string(), + COLOR_B, + engine::point(-0.25, 0.75, -0.25) + ), + Point::new( + "b3".to_string(), + "B₃".to_string(), + COLOR_B, + engine::point(-0.25, -0.25, 0.75) + ), + Point::new( + "c1".to_string(), + "C₁".to_string(), + COLOR_C, + engine::point(0.0, -1.0, -1.0) + ), + Point::new( + "c2".to_string(), + "C₂".to_string(), + COLOR_C, + engine::point(-1.0, 0.0, -1.0) + ), + Point::new( + "c3".to_string(), + "C₃".to_string(), + COLOR_C, + engine::point(-1.0, -1.0, 0.0) + ) + ]; + 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(); + let frac_2_sqrt_6 = 2.0 * frac_1_sqrt_6; + let faces = [ + Sphere::new( + "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) + ), + 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) + ), + 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) + ) + ]; + 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 + let face = assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[&format!("face{j}")].clone() + ); + let curvature_regulator = face.regulators().with_untracked( + |regs| regs.first().unwrap().clone() + ); + 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()); + 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( + |elts_by_id| elts_by_id[&format!("{series}{j}")].clone() + ) + ); + 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()); + 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( + |index| assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[&format!("{series}{index}")].clone() + ) + ) + ); + for pair in adjacent_pairs { + assembly.insert_regulator( + Rc::new(InversiveDistanceRegulator::new(pair)) + ); + } + } +} + +// to finish describing the dodecahedral circle packing, set the inversive +// distance regulators to -1. some of the regulators have already been set +fn load_dodeca_packing_assemb(assembly: &Assembly) { + // add the substrate + let _ = assembly.try_insert_element( + Sphere::new( + "substrate".to_string(), + "Substrate".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(0.0, 0.0, 0.0, 1.0) + ) + ); + 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 + let phi_inv = 1.0 / phi; + let coord_scale = (phi + 2.0).sqrt(); + let face_scales = [phi_inv, (13.0 / 12.0) / coord_scale]; + let face_radii = [phi_inv, 5.0 / 12.0]; + let mut faces = Vec::>::new(); + let subscripts = ["₀", "₁"]; + for j in 0..2 { + 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( + Sphere::new( + id_a.clone(), + format!("A{label_sub}"), + COLOR_A, + engine::sphere(0.0, small_coord, big_coord, face_radii[k]) + ) + ); + faces.push( + assembly.elements_by_id.with_untracked( + |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( + Sphere::new( + id_b.clone(), + format!("B{label_sub}"), + COLOR_B, + engine::sphere(small_coord, big_coord, 0.0, face_radii[k]) + ) + ); + faces.push( + assembly.elements_by_id.with_untracked( + |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( + Sphere::new( + id_c.clone(), + format!("C{label_sub}"), + COLOR_C, + engine::sphere(big_coord, 0.0, small_coord, face_radii[k]) + ) + ); + faces.push( + assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[&id_c].clone() + ) + ); + } + } + + // 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 { + let long_edge_ids = [ + format!("{long_edge_plane}{k}0"), + format!("{long_edge_plane}{k}1") + ]; + let short_edge_ids = [ + format!("{short_edge_plane}0{k}"), + format!("{short_edge_plane}1{k}") + ]; + let [long_edge, short_edge] = [long_edge_ids, short_edge_ids].map( + |edge_ids| edge_ids.map( + |id| assembly.elements_by_id.with_untracked( + |elts_by_id| elts_by_id[&id].clone() + ) + ) + ); + + // 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 { + let side_tangency = InversiveDistanceRegulator::new( + [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()); + } + assembly.insert_regulator(Rc::new(side_tangency)); + } + } + } + } +} + +// the initial configuration of this test assembly deliberately violates the +// constraints, so loading the assembly will trigger a non-trivial realization +fn load_balanced_assemb(assembly: &Assembly) { + // create the spheres + const R_OUTER: f64 = 10.0; + const R_INNER: f64 = 4.0; + let spheres = [ + Sphere::new( + "outer".to_string(), + "Outer".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(0.0, 0.0, 0.0, R_OUTER) + ), + Sphere::new( + "a".to_string(), + "A".to_string(), + [1.00_f32, 0.00_f32, 0.25_f32], + engine::sphere(0.0, 4.0, 0.0, R_INNER) + ), + Sphere::new( + "b".to_string(), + "B".to_string(), + [0.00_f32, 0.25_f32, 1.00_f32], + engine::sphere(0.0, -4.0, 0.0, R_INNER) + ), + ]; + 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), + (a.clone(), R_INNER), + (b.clone(), R_INNER) + ] { + let curvature_regulator = sphere.regulators().with_untracked( + |regs| regs.first().unwrap().clone() + ); + let curvature = 0.5 / radius; + curvature_regulator.set_point().set( + 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()); + assembly.insert_regulator(Rc::new(tangency)); + } +} + +// the initial configuration of this test assembly deliberately violates the +// constraints, so loading the assembly will trigger a non-trivial realization +fn load_off_center_assemb(assembly: &Assembly) { + // create a point almost at the origin and a sphere centered on the origin + let _ = assembly.try_insert_element( + Point::new( + "point".to_string(), + "Point".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::point(1e-9, 0.0, 0.0) + ), + ); + let _ = assembly.try_insert_element( + Sphere::new( + "sphere".to_string(), + "Sphere".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + 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()); + assembly.insert_regulator(Rc::new(incidence)); +} + +// setting the inversive distances between the vertices to -2 gives a regular +// tetrahedron with side length 1, whose insphere and circumsphere have radii +// sqrt(1/6) and sqrt(3/2), respectively. to measure those radii, set an +// inversive distance of -1 between the insphere and each face, and then set an +// inversive distance of 0 between the circumsphere and each vertex +fn load_radius_ratio_assemb(assembly: &Assembly) { + let index_range = 1..=4; + + // create the spheres + const GRAY: ElementColor = [0.75_f32, 0.75_f32, 0.75_f32]; + let spheres = [ + Sphere::new( + "sphere_faces".to_string(), + "Insphere".to_string(), + GRAY, + engine::sphere(0.0, 0.0, 0.0, 0.5) + ), + Sphere::new( + "sphere_vertices".to_string(), + "Circumsphere".to_string(), + GRAY, + engine::sphere(0.0, 0.0, 0.0, 0.25) + ) + ]; + for sphere in spheres { + let _ = assembly.try_insert_element(sphere); + } + + // create the vertices + let vertices = izip!( + index_range.clone(), + [ + [1.00_f32, 0.50_f32, 0.75_f32], + [1.00_f32, 0.75_f32, 0.50_f32], + [1.00_f32, 1.00_f32, 0.50_f32], + [0.75_f32, 0.50_f32, 1.00_f32] + ].into_iter(), + [ + engine::point(-0.6, -0.8, -0.6), + engine::point(-0.6, 0.8, 0.6), + engine::point(0.6, -0.8, 0.6), + engine::point(0.6, 0.8, -0.6) + ].into_iter() + ).map( + |(k, color, representation)| { + Point::new( + format!("v{k}"), + format!("Vertex {k}"), + color, + representation + ) + } + ); + 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)); + let faces = izip!( + index_range.clone(), + [ + [1.00_f32, 0.00_f32, 0.25_f32], + [1.00_f32, 0.25_f32, 0.00_f32], + [0.75_f32, 0.75_f32, 0.00_f32], + [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) + ].into_iter() + ).map( + |(k, color, representation)| { + Sphere::new( + format!("f{k}"), + format!("Face {k}"), + color, + representation + ) + } + ); + for face in faces { + face.ghost().set(true); + let _ = assembly.try_insert_element(face); + } + + // impose the constraints + for j in index_range.clone() { + let [face_j, vertex_j] = [ + format!("f{j}"), + format!("v{j}") + ].map( + |id| assembly.elements_by_id.with_untracked( + |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() + ); + 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( + [vertex_j.clone(), vertex_k.clone()] + ); + 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()); + assembly.insert_regulator(Rc::new(incidence_regulator)); + } + } +} + +// to finish setting up the problem, fix the following curvatures: +// sun 1 +// moon 5/3 = 1.666666666666666... +// chain1 2 +// a tiny `x` or `z` nudge of the outer sphere reliably prevents realization +// failures before they happen, or resolves them after they happen. the result +// depends sensitively on the translation direction, suggesting that realization +// is failing because the engine is having trouble breaking a symmetry +// /* TO DO */ +// the engine's performance on this problem is scale-dependent! with the current +// initial conditions, realization fails for any order of imposing the remaining +// curvature constraints. scaling everything up by a factor of ten, as done in +// the original problem, makes realization succeed reliably. one potentially +// relevant difference is that a lot of the numbers in the current initial +// conditions are exactly representable as floats, unlike the analogous numbers +// in the scaled-up problem. the inexact representations might break the +// symmetry that's getting the engine stuck +fn load_irisawa_hexlet_assemb(assembly: &Assembly) { + let index_range = 1..=6; + let colors = [ + [1.00_f32, 0.00_f32, 0.25_f32], + [1.00_f32, 0.25_f32, 0.00_f32], + [0.75_f32, 0.75_f32, 0.00_f32], + [0.25_f32, 1.00_f32, 0.00_f32], + [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( + "outer".to_string(), + "Outer".to_string(), + [0.5_f32, 0.5_f32, 0.5_f32], + engine::sphere(0.0, 0.0, 0.0, 1.5) + ), + Sphere::new( + "sun".to_string(), + "Sun".to_string(), + [0.75_f32, 0.75_f32, 0.75_f32], + engine::sphere(0.0, -0.75, 0.0, 0.75) + ), + Sphere::new( + "moon".to_string(), + "Moon".to_string(), + [0.25_f32, 0.25_f32, 0.25_f32], + engine::sphere(0.0, 0.75, 0.0, 0.75) + ), + ].into_iter().chain( + index_range.clone().zip(colors).map( + |(k, color)| { + let ang = (k as f64) * PI/3.0; + Sphere::new( + format!("chain{k}"), + format!("Chain {k}"), + color, + engine::sphere(1.0 * ang.sin(), 0.0, 1.0 * ang.cos(), 0.5) + ) + } + ) + ); + 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() + ); + outer.ghost().set(true); + let outer_curvature_regulator = outer.regulators().with_untracked( + |regs| regs.first().unwrap().clone() + ); + 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( + |elts_by_id| elts_by_id[id].clone() + ) + ); + let chain = index_range.map( + |k| assembly.elements_by_id.with_untracked( + |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 (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()); + 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)); +} + +// --- chooser --- + +/* DEBUG */ +#[component] +pub fn TestAssemblyChooser() -> View { + // create an effect that loads the selected test assembly + let assembly_name = create_signal("general".to_string()); + create_effect(move || { + // get name of chosen assembly + let name = assembly_name.get_clone(); + console::log_1( + &JsValue::from(format!("Showing assembly \"{}\"", name.clone())) + ); + + batch(|| { + let state = use_context::(); + let assembly = &state.assembly; + + // pause realization + assembly.keep_realized.set(false); + + // 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_gen_assemb(assembly), + "low-curv" => load_low_curv_assemb(assembly), + "pointed" => load_pointed_assemb(assembly), + "tridim-icosahedron" => load_tridim_icosahedron_assemb(assembly), + "dodeca-packing" => load_dodeca_packing_assemb(assembly), + "balanced" => load_balanced_assemb(assembly), + "off-center" => load_off_center_assemb(assembly), + "radius-ratio" => load_radius_ratio_assemb(assembly), + "irisawa-hexlet" => load_irisawa_hexlet_assemb(assembly), + _ => () + }; + + // resume realization + assembly.keep_realized.set(true); + }); + }); + + // build the chooser + view! { + select(bind:value=assembly_name) { + option(value="general") { "General" } + option(value="low-curv") { "Low-curvature" } + option(value="pointed") { "Pointed" } + option(value="tridim-icosahedron") { "Tridiminished icosahedron" } + option(value="dodeca-packing") { "Dodecahedral packing" } + option(value="balanced") { "Balanced" } + option(value="off-center") { "Off-center" } + option(value="radius-ratio") { "Radius ratio" } + option(value="irisawa-hexlet") { "Irisawa hexlet" } + option(value="empty") { "Empty" } + } + } +} \ No newline at end of file