use itertools::izip; use std::{f64::consts::{FRAC_1_SQRT_2, PI, SQRT_2}, rc::Rc}; use nalgebra::Vector3; use sycamore::prelude::*; use web_sys::{console, wasm_bindgen::JsValue}; use crate::{ AppState, assembly::{ Assembly, Element, ElementColor, InversiveDistanceRegulator, Point, PointCoordinateRegulator, Regulator, Sphere, }, engine, engine::{DescentHistory, point}, specified::SpecifiedValue, }; // Convenience: macro to allow elision of const array lengths // adapted from https://stackoverflow.com/a/59905715 macro_rules! const_array { ($name: ident: $ty: ty = $value: expr) => { const $name: [$ty; $value.len()] = $value; } } // Convenience: use sqrt() as a function to get the square root of // anything that can be losslessly converted to f64, since we happen // to always want our sqrts to be f64. // RUST KVETCHES: (I) Why is this so convoluted? // In particular, what would be so bad about allowing // const fn sqrt>(x: T) -> f64 { (x as f64).sqrt() } // ??? // (II) Oh dear, sqrt is not computable in a const context, so we have to // roll our own. I hope I get it right! I definitely don't know how to ensure // that the final double is correctly rounded :-( const SIXTH: f64 = 1./6.; const FOURTH: f64 = 1./4.; const fn invsqrt(xin: f64) -> f64 { if !xin.is_finite() { return f64::NAN; } let mut log4 = 0; let mut x = xin; while x < 1. { x *= 4.; log4 -= 1; } while x > 4. { x *= FOURTH; log4 += 1; } let mut next = 1.1 - SIXTH * x; let mut diff = 1.; while diff > 4. * f64::EPSILON { // termination condition?? let last = next; next = last * (1.5 - 0.5 * x * last * last); diff = next - last; if diff < 0. { diff *= -1.; } } while log4 > 0 { next *= 0.5; log4 -= 1; } while log4 < 0 { next *= 2.; log4 += 1; } return next; } #[const_trait] trait ConstSqrt {fn sqr(self) -> f64;} impl const ConstSqrt for f64 {fn sqr(self) -> f64 {self * invsqrt(self)}} impl const ConstSqrt for i32 {fn sqr(self) -> f64 {(self as f64).sqr()}} const fn sqrt(x: T) -> f64 {x.sqr()} // RUST KVETCHES: (I) It is annoying that we must redundantly specify // the type of the well-typed RHS to assign it to a const. // See https://github.com/rust-lang/rfcs/pull/3546 // Can we fix this in husht? Seems like we will need to be able to do full // rust type inference in the transpiler; how will we accomplish that? // (2) It is very annoying that there doesn't seem to be any way to specify // that we want an expression like `5.0 / 2` to be evaluated by converting // the 2 to a float, because of the "orphan rule". Can we fix this in husht? // Again, it seems we would need full rust type inference. // FIXME: replace with std::f64::consts::PHI when that gets stabilized const PHI: f64 = (1. + sqrt(5)) / 2.; const fn gray(level: f32) -> ElementColor { [level, level, level] } const GRAY: ElementColor = gray(0.75); const RED: ElementColor = [1., 0., 0.25]; const GREEN: ElementColor = [0.25, 1., 0.]; const BLUE: ElementColor = [0., 0.25, 1.]; // --- 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_general(assembly: &Assembly) { let _ = assembly.try_insert_element( Sphere::new( "gemini_a", "Castor", [1.00, 0.25, 0.00], engine::sphere(0.5, 0.5, 0.0, 1.0), ) ); let _ = assembly.try_insert_element( Sphere::new( "gemini_b", "Pollux", BLUE, engine::sphere(-0.5, -0.5, 0.0, 1.0), ) ); let _ = assembly.try_insert_element( Sphere::new( "ursa_major", "Ursa major", [0.25, 0.00, 1.00], engine::sphere(-0.5, 0.5, 0.0, 0.75), ) ); let _ = assembly.try_insert_element( Sphere::new( "ursa_minor", "Ursa minor", GREEN, engine::sphere(0.5, -0.5, 0.0, 0.5), ) ); let _ = assembly.try_insert_element( Sphere::new( "moon_deimos", "Deimos", [0.75, 0.75, 0.00], engine::sphere(0.0, 0.15, 1.0, 0.25), ) ); let _ = assembly.try_insert_element( Sphere::new( "moon_phobos", "Phobos", [0.00, 0.75, 0.50], engine::sphere(0.0, -0.15, -1.0, 0.25), ) ); } fn load_low_curvature(assembly: &Assembly) { // create the spheres const A: f64 = sqrt(0.75); let _ = assembly.try_insert_element( Sphere::new( "central", "Central", GRAY, engine::sphere(0.0, 0.0, 0.0, 1.0), ) ); let _ = assembly.try_insert_element( Sphere::new( "assemb_plane", "Assembly plane", GRAY, engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0), ) ); let _ = assembly.try_insert_element( Sphere::new( "side1", "Side 1", RED, engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0), ) ); let _ = assembly.try_insert_element( Sphere::new( "side2", "Side 2", GREEN, engine::sphere_with_offset(-0.5, A, 0.0, 1.0, 0.0), ) ); let _ = assembly.try_insert_element( Sphere::new( "side3", "Side 3", BLUE, engine::sphere_with_offset(-0.5, -A, 0.0, 1.0, 0.0), ) ); let _ = assembly.try_insert_element( Sphere::new( "corner1", "Corner 1", GRAY, engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0), ) ); let _ = assembly.try_insert_element( Sphere::new( "corner2", "Corner 2", GRAY, engine::sphere(2.0/3.0, -4.0/3.0 * A, 0.0, 1.0/3.0), ) ); let _ = assembly.try_insert_element( Sphere::new( "corner3", "Corner 3", GRAY, 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.find_element(id) ); let sides = index_range.clone().map( |k| assembly.find_element(&format!("side{k}")) ); let corners = index_range.map( |k| assembly.find_element(&format!("corner{k}")) ); 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(assembly: &Assembly) { let _ = assembly.try_insert_element( Point::new( "point_front", "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( "point_back", "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_tridiminished_icosahedron(assembly: &Assembly) { // create the vertices const COLOR_A: ElementColor = [1.00, 0.25, 0.25]; const COLOR_B: ElementColor = [0.75, 0.75, 0.75]; const COLOR_C: ElementColor = [0.25, 0.50, 1.00]; let vertices = [ Point::new("a1", "A₁", COLOR_A, point( 0.25, 0.75, 0.75)), Point::new("a2", "A₂", COLOR_A, point( 0.75, 0.25, 0.75)), Point::new("a3", "A₃", COLOR_A, point( 0.75, 0.75, 0.25)), Point::new("b1", "B₁", COLOR_B, point( 0.75, -0.25, -0.25)), Point::new("b2", "B₂", COLOR_B, point(-0.25, 0.75, -0.25)), Point::new("b3", "B₃", COLOR_B, point(-0.25, -0.25, 0.75)), Point::new("c1", "C₁", COLOR_C, point( 0.0, -1.0, -1.0)), Point::new("c2", "C₂", COLOR_C, point(-1.0, 0.0, -1.0)), Point::new("c3", "C₃", COLOR_C, point(-1.0, -1.0, 0.0)), ]; for vertex in vertices { let _ = assembly.try_insert_element(vertex); } // create the faces const SQRT_1_6: f64 = invsqrt(6.); const SQRT_2_3: f64 = 2. * SQRT_1_6; let faces = [ Sphere::new( "face1", "Face 1", GRAY, engine::sphere_with_offset( SQRT_2_3, -SQRT_1_6, -SQRT_1_6, -SQRT_1_6, 0.0), ), Sphere::new( "face2", "Face 2", GRAY, engine::sphere_with_offset( -SQRT_1_6, SQRT_2_3, -SQRT_1_6, -SQRT_1_6, 0.0), ), Sphere::new( "face3", "Face 3", GRAY, engine::sphere_with_offset( -SQRT_1_6, -SQRT_1_6, SQRT_2_3, -SQRT_1_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_dodecahedral_packing(assembly: &Assembly) { // add the substrate let _ = assembly.try_insert_element( Sphere::new( "substrate", "Substrate", GRAY, 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]; const PHI_INV: f64 = 1.0 / PHI; const COORD_SCALE: f64 = sqrt(PHI + 2.0); const_array!(FACE_SCALES: f64 = [PHI_INV, (13.0 / 12.0) / COORD_SCALE]); const_array!(FACE_RADII: f64 = [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] * if j > 0 {1.} else {-1.}; let big_coord = FACE_SCALES[k] * PHI * if k > 0 {1.} else {-1.}; 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, &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, &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, &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(assembly: &Assembly) { // create the spheres const R_OUTER: f64 = 10.0; const R_INNER: f64 = 4.0; let spheres = [ Sphere::new( "outer","Outer", GRAY, engine::sphere(0.0, 0.0, 0.0, R_OUTER)), Sphere::new("a", "A", RED, engine::sphere(0.0, 4.0, 0.0, R_INNER)), Sphere::new("b", "B", BLUE, 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.find_element(id) ); // 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(assembly: &Assembly) { // create a point almost at the origin and a sphere centered on the origin let _ = assembly.try_insert_element( Point::new("point", "Point", GRAY, point(1e-9, 0.0, 0.0)), ); let _ = assembly.try_insert_element( Sphere::new( "sphere", "Sphere", GRAY, engine::sphere(0.0, 0.0, 0.0, 1.0)), ); // get references to the elements let point_and_sphere = ["point", "sphere"].map( |id| assembly.find_element(id) ); // put the point on the sphere let incidence = InversiveDistanceRegulator::new(point_and_sphere); incidence.set_to(0.); 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(assembly: &Assembly) { let index_range = 1..=4; // create the spheres let spheres = [ Sphere::new( "sphere_faces", "Insphere", GRAY, engine::sphere(0.0, 0.0, 0.0, 0.5), ), Sphere::new( "sphere_vertices", "Circumsphere", 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.find_element(&id)); // make the faces planar let curvature_regulator = face_j.regulators().with_untracked( |regs| regs.first().unwrap().clone() ); curvature_regulator.set_to(0.); for k in index_range.clone().filter(|&index| index != j) { let vertex_k = assembly.find_element(&format!("v{k}")); // 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_to(0.); 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(assembly: &Assembly) { let index_range = 1..=6; let colors = [ [1.00, 0.00, 0.25], [1.00, 0.25, 0.00], [0.75, 0.75, 0.00], [0.25, 1.00, 0.00], [0.00, 0.25, 1.00], [0.25, 0.00, 1.00], ].into_iter(); // create the spheres let spheres = [ Sphere::new( "outer", "Outer", gray(0.5), engine::sphere(0.0, 0.0, 0.0, 1.5), ), Sphere::new( "sun", "Sun", GRAY, engine::sphere(0.0, -0.75, 0.0, 0.75), ), Sphere::new( "moon", "Moon", gray(0.25), 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.find_element("outer"); 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 [sun, moon] = ["sun", "moon"].map(|id| assembly.find_element(id)); let chain = index_range.map( |k| assembly.find_element(&format!("chain{k}")) ); 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)); } const HPHI: f64 = PHI / 2.; // "half phi" const RTHPHI: f64 = sqrt(HPHI); // "root half phi" const RTPHIPH: f64 = sqrt(PHI + 0.5); // "root phi plus (half)" const P: bool = true; // Pinned const F: bool = false; // Free // Initial data for the vertices commmon to 554aug2 and 554domed. // Used the Vectornaut near_miss branch final positions for 554aug2, // to the 3 decimal places currently shown. const_array!(ACRON554_COMMON: (&str, [f64; 3], bool, usize, &str) = [ // id, coordinates, Pin/Free, level, earlier neighbor IDs. ("A_NE", [ 0.5, 0.5, 0.], P, 0, ""), ("A_NW", [-0.5, 0.5, 0.], P, 0, ""), ("A_SE", [ 0.5, -0.5, 0.], P, 0, ""), ("A_SW", [-0.5, -0.5, 0.], P, 0, ""), ("Z_E", [ 0.229, -0.002, 0.821], F, 1, "A_NE,A_SE"), ("Z_S", [ 0.002, -0.229, 0.821], F, 1, "A_SE,A_SW"), ("B_NE", [ HPHI, HPHI, RTHPHI], P, 2, "Z_E"), ("B_NW", [-HPHI, HPHI, RTHPHI], P, 2, ""), ("B_SW", [-HPHI, -HPHI, RTHPHI], P, 2, "Z_S"), ("B_SE", [ 0.812, -0.812, 0.89], F, 2, "Z_E,Z_S"), ("Y_NE", [ 0.11, 0.103, 1.019], F, 3, "B_NE"), ("Y_NW", [-0.103, 0.103, 1.02], F, 3, "B_NW"), ("Y_SE", [ 0.11, -0.11, 1.017], F, 3, "B_SE"), ("Y_SW", [-0.103, -0.11, 1.019], F, 3, "B_SW"), ("C_N", [ 0., 1., RTPHIPH], P, 4, "Y_NE,Y_NW"), ("C_W", [-1., 0., RTPHIPH], P, 4, "Y_NW,Y_SW"), ("C_E", [ 1.006, -0.006, 1.45], F, 4, "Y_NE,Y_SE"), ("C_S", [ 0.006, -1.006, 1.45], F, 4, "Y_SE,Y_SW"), ("D_NE", [ 0.2, 0.181, 2.011], F, 5, "Y_NE,C_N,C_E"), ("D_NW", [-0.181, 0.181, 2.014], F, 5, "Y_NW,C_N,C_W"), ("D_SE", [ 0.2, -0.2, 2.009], F, 5, "Y_SE,C_E,C_S"), ("D_SW", [-0.181, -0.2, 2.011], F, 5, "Y_SW,C_W,C_S"), ("E_N", [ 0.012, 1.055, 2.46], F, 6, "C_N,D_NE,D_NW"), ("E_W", [-1.055, -0.012, 2.46], F, 6, "C_W,D_NW,D_SW"), ("E_E", [ 1.079, -0.012, 2.447], F, 6, "C_E,D_NE,D_SE"), ("E_S", [ 0.012, -1.079, 2.447], F, 6, "C_S,D_SE,D_SW"), ("F_NE", [ 0.296, 0.265, 3.003], F, 7, "D_NE,E_N,E_E"), ("F_NW", [-0.265, 0.265, 3.007], F, 7, "D_NW,E_N,E_W"), ("F_SE", [ 0.296, -0.296, 3.0], F, 7, "D_SE,E_E,E_S"), ("F_SW", [-0.265, -0.296, 3.003], F, 7, "D_SW,E_W,E_S"), // The following must be in order around the octagon (in some orientation) ("G_1", [ 0.517, 1.19, 3.312], F, 8, "E_N,F_NE"), ("G_2", [ 1.224, 0.483, 3.304], F, 8, "E_E,F_NE"), ("G_4", [ 1.224, -0.517, 3.298], F, 8, "E_E,F_SE"), ("G_5", [ 0.517, -1.224, 3.298], F, 8, "E_S,F_SE"), ("G_7", [-0.483, -1.224, 3.304], F, 8, "E_S,F_SW"), ("G_8", [-1.19, -0.517, 3.312], F, 8, "E_W,F_SW"), ("G_10", [-1.19, 0.483, 3.318], F, 8, "E_W,F_NW"), ("G_11", [-0.483, 1.19, 3.318], F, 8, "E_N,F_NW"), ]); const_array!(LEVEL_COLORS: ElementColor = [ [0.75, 0., 0.75], [1., 0.4, 0.6], [1., 0., 0.25], [1., 0.75, 0.25], [0.75, 0.5, 0.], [0.9, 0.9, 0.], [0.25, 0.75, 0.], [0., 0.5, 0.75], [0.25, 0., 1.], ]); fn load_554aug2(assembly: &Assembly) { // first a plane for the octagon let oct_id = "f_g"; let engsph = engine::sphere_with_offset( 0., 0., 1., ACRON554_COMMON[37].1[2], 0.); let octaface = Sphere::new(oct_id, "Octagon", [0.7, 0.7, 0.7], engsph); octaface.ghost().set(true); assembly.try_insert_element(octaface); let face_rc = assembly.find_element(oct_id); let face_curvature = face_rc.regulators().with_untracked( |regs| regs.first().unwrap().clone() ); face_curvature.set_to(0.); // Octagon vertices and side/diagonal lengths let mut oct_verts = Vec::>::new(); const OCT_LONG: usize = 4; const OCT_N_DIAG: usize = 5; const OCT_N: usize = 8; const OCT_DIST: [f64; OCT_N_DIAG] = [0., // dummy at start -0.5, -0.5*(2. + SQRT_2), -0.5*(3. + 2.*SQRT_2), -0.5*(4. + 2.*SQRT_2) ]; // Now process the acron data for (id, v, pinned, l, _neighbors) in ACRON554_COMMON { let pt = Point::new(id, id, LEVEL_COLORS[l], point(v[0], v[1], v[2])); assembly.try_insert_element(pt); if pinned { // regulate each coordinate to its given value let mut freeze = Vec::>::new(); // QUESTION: Would there be a way to insert an Rc into // an assembly to avoid the need to re-lookup pt in the assembly // after just inserting it? let pt_rc = assembly.elements_by_id.with_untracked( |elts| elts[id].clone() ); // filter the three coordinate regulators into freeze pt_rc.regulators().with_untracked( |regs| { for reg in regs { if let Some(_pcr) = reg .as_any().downcast_ref::() { freeze.push(reg.clone()) } } }); // now set them to their originally specified values. let mut coord: usize = 0; for reg in freeze { reg.set_to(v[coord]); coord += 1; } } // If part of the octagon, make incident to the plane: if l == 8 { let pt_rc = assembly.find_element(id); let oct_index = oct_verts.len(); oct_verts.push(pt_rc.clone()); let incidence = InversiveDistanceRegulator::new( [face_rc.clone(), pt_rc.clone()]); incidence.set_to(0.0); assembly.insert_regulator(Rc::new(incidence)); // And regulate the length to the other vertices of the octagon for offset in 1..OCT_N_DIAG { if offset <= oct_index { let dist = InversiveDistanceRegulator::new( [oct_verts[oct_index - offset].clone(), pt_rc.clone()]); dist.set_to(OCT_DIST[offset]); assembly.insert_regulator(Rc::new(dist)); } if offset < OCT_LONG && oct_index + offset >= OCT_N { let forward = oct_index + offset - OCT_N; let dist = InversiveDistanceRegulator::new( [oct_verts[forward].clone(), pt_rc.clone()]); dist.set_to(OCT_DIST[offset]); assembly.insert_regulator(Rc::new(dist)); } } } } } // --- 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; // clear state assembly.regulators.update(|regs| regs.clear()); assembly.elements.update(|elts| elts.clear()); assembly.elements_by_id.update(|elts_by_id| elts_by_id.clear()); assembly.descent_history.set(DescentHistory::new()); state.selection.update(|sel| sel.clear()); // load assembly match name.as_str() { "general" => load_general(assembly), "low-curvature" => load_low_curvature(assembly), "pointed" => load_pointed(assembly), "tridiminished-icosahedron" => load_tridiminished_icosahedron(assembly), "dodecahedral-packing" => load_dodecahedral_packing(assembly), "balanced" => load_balanced(assembly), "off-center" => load_off_center(assembly), "radius-ratio" => load_radius_ratio(assembly), "irisawa-hexlet" => load_irisawa_hexlet(assembly), "aug554" => load_554aug2(assembly), _ => (), }; }); }); // FIXME: Non-DRY -- should not need to reiterate thie list of assembly // labels // build the chooser view! { select(bind:value = assembly_name) { option(value = "general") { "General" } option(value = "low-curvature") { "Low-curvature" } option(value = "pointed") { "Pointed" } option(value = "tridiminished-icosahedron") { "Tridiminished icosahedron" } option(value = "dodecahedral-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 = "aug554") { "McNeill acron 554" } option(value = "empty") { "Empty" } } } }