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 }; /* 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) ) ); } } } /* 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), "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( on:click=|_| { let state = use_context::(); state.assembly.insert_element_default::(); } ) { "Add sphere" } button( on:click=|_| { let state = use_context::(); state.assembly.insert_element_default::(); } ) { "Add point" } button( class="emoji", /* KLUDGE */ // for convenience, we're using an emoji as a temporary icon for this button disabled={ let state = use_context::(); state.selection.with(|sel| sel.len() != 2) }, on:click=|_| { let state = use_context::(); let subjects: [_; 2] = state.selection.with( // the button is only enabled when two elements are // selected, so we know the cast to a two-element array // will succeed |sel| sel .clone() .into_iter() .collect::>() .try_into() .unwrap() ); state.assembly.insert_regulator( Rc::new(InversiveDistanceRegulator::new(subjects)) ); 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="balanced") { "Balanced" } option(value="off-center") { "Off-center" } option(value="radius-ratio") { "Radius ratio" } option(value="irisawa-hexlet") { "Irisawa hexlet" } option(value="empty") { "Empty" } } } } }