Add more test assemblies (#103)

This PR helps probe the capabilities of the engine.

Also adjusts the realization triggering system to reduce redundant realizations as we set an assembly's regulators during loading. Specificially, consolidates all calls to `realize()` into a single effect, which is triggered by the `needs_realization` signal.
Also introduces a `keep_realized` signal and use it to pause realization while loading assemblies, but this signal is planned for removal as ultimately we do not want a separate "mode" of interpreting commands during loading, for maximal reproducibility of results (and simplicity of system).

Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Reviewed-on: StudioInfinity/dyna3#103
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
This commit is contained in:
Vectornaut 2025-07-22 22:01:37 +00:00 committed by Glen Whitney
parent 5864017e6f
commit 0801200210
13 changed files with 1045 additions and 277 deletions

View file

@ -1,259 +0,0 @@
use std::{f64::consts::FRAC_1_SQRT_2, rc::Rc};
use sycamore::prelude::*;
use web_sys::{console, wasm_bindgen::JsValue};
use crate::{
AppState,
engine,
engine::DescentHistory,
assembly::{Assembly, InversiveDistanceRegulator, Point, Sphere}
};
/* 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) {
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)
)
);
}
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)
)
);
}
}
}
#[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::<AppState>();
let assembly = &state.assembly;
// clear state
assembly.regulators.update(|regs| regs.clear());
assembly.elements.update(|elts| elts.clear());
assembly.elements_by_id.update(|elts_by_id| elts_by_id.clear());
assembly.descent_history.set(DescentHistory::new());
state.selection.update(|sel| sel.clear());
// load assembly
match name.as_str() {
"general" => load_gen_assemb(assembly),
"low-curv" => load_low_curv_assemb(assembly),
"pointed" => load_pointed_assemb(assembly),
_ => ()
};
});
});
view! {
div(id="add-remove") {
button(
on:click=|_| {
let state = use_context::<AppState>();
state.assembly.insert_element_default::<Sphere>();
}
) { "Add sphere" }
button(
on:click=|_| {
let state = use_context::<AppState>();
state.assembly.insert_element_default::<Point>();
}
) { "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::<AppState>();
state.selection.with(|sel| sel.len() != 2)
},
on:click=|_| {
let state = use_context::<AppState>();
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::<Vec<_>>()
.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="empty") { "Empty" }
}
}
}
}

View file

@ -13,7 +13,7 @@ use sycamore::prelude::*;
use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */
use crate::{
display::DisplayItem,
components::{display::DisplayItem, outline::OutlineItem},
engine::{
Q,
change_half_curvature,
@ -29,7 +29,6 @@ use crate::{
DescentHistory,
Realization
},
outline::OutlineItem,
specified::SpecifiedValue
};
@ -552,6 +551,10 @@ pub struct Assembly {
// indexing
pub elements_by_id: Signal<BTreeMap<String, Rc<dyn Element>>>,
// realization control
pub keep_realized: Signal<bool>,
pub needs_realization: Signal<bool>,
// realization diagnostics
pub realization_status: Signal<Result<(), String>>,
pub descent_history: Signal<DescentHistory>
@ -559,14 +562,30 @@ pub struct Assembly {
impl Assembly {
pub fn new() -> Assembly {
Assembly {
// create an assembly
let assembly = Assembly {
elements: create_signal(BTreeSet::new()),
regulators: create_signal(BTreeSet::new()),
tangent: create_signal(ConfigSubspace::zero(0)),
elements_by_id: create_signal(BTreeMap::default()),
keep_realized: create_signal(true),
needs_realization: create_signal(false),
realization_status: create_signal(Ok(())),
descent_history: create_signal(DescentHistory::new())
}
};
// realize the assembly whenever it becomes simultaneously true that
// we're trying to keep it realized and it needs realization
let assembly_for_effect = assembly.clone();
create_effect(move || {
let should_realize = assembly_for_effect.keep_realized.get()
&& assembly_for_effect.needs_realization.get();
if should_realize {
assembly_for_effect.realize();
}
});
assembly
}
// --- inserting elements and regulators ---
@ -627,7 +646,7 @@ impl Assembly {
regulators.update(|regs| regs.insert(regulator.clone()));
}
// update the realization when the regulator becomes a constraint, or is
// request a realization when the regulator becomes a constraint, or is
// edited while acting as a constraint
let self_for_effect = self.clone();
create_effect(move || {
@ -636,7 +655,7 @@ impl Assembly {
console_log!("Updated regulator with subjects {:?}", regulator.subjects());
if regulator.try_activate() {
self_for_effect.realize();
self_for_effect.needs_realization.set(true);
}
});
@ -731,6 +750,9 @@ impl Assembly {
// save the tangent space
self.tangent.set_silent(tangent);
// clear the realization request flag
self.needs_realization.set(false);
},
Err(message) => {
// report the realization status. the `Err(message)` we're
@ -826,10 +848,10 @@ impl Assembly {
});
}
// bring the configuration back onto the solution variety. this also
// gets the elements' column indices and the saved tangent space back in
// sync
self.realize();
// request a realization to bring the configuration back onto the
// solution variety. this also gets the elements' column indices and the
// saved tangent space back in sync
self.needs_realization.set(true);
}
}

View file

@ -0,0 +1,5 @@
pub mod add_remove;
pub mod diagnostics;
pub mod display;
pub mod outline;
pub mod test_assembly_chooser;

View file

@ -0,0 +1,54 @@
use std::rc::Rc;
use sycamore::prelude::*;
use super::test_assembly_chooser::TestAssemblyChooser;
use crate::{
AppState,
assembly::{InversiveDistanceRegulator, Point, Sphere}
};
#[component]
pub fn AddRemove() -> View {
view! {
div(id="add-remove") {
button(
on:click=|_| {
let state = use_context::<AppState>();
state.assembly.insert_element_default::<Sphere>();
}
) { "Add sphere" }
button(
on:click=|_| {
let state = use_context::<AppState>();
state.assembly.insert_element_default::<Point>();
}
) { "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::<AppState>();
state.selection.with(|sel| sel.len() != 2)
},
on:click=|_| {
let state = use_context::<AppState>();
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::<Vec<_>>()
.try_into()
.unwrap()
);
state.assembly.insert_regulator(
Rc::new(InversiveDistanceRegulator::new(subjects))
);
state.selection.update(|sel| sel.clear());
}
) { "🔗" }
TestAssemblyChooser {}
}
}
}

View file

@ -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::<Rc<dyn Element>>::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::<AppState>();
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" }
}
}
}

View file

@ -1,9 +1,6 @@
mod add_remove;
mod assembly;
mod diagnostics;
mod display;
mod components;
mod engine;
mod outline;
mod specified;
#[cfg(test)]
@ -12,11 +9,13 @@ mod tests;
use std::{collections::BTreeSet, rc::Rc};
use sycamore::prelude::*;
use add_remove::AddRemove;
use assembly::{Assembly, Element};
use diagnostics::Diagnostics;
use display::Display;
use outline::Outline;
use components::{
add_remove::AddRemove,
diagnostics::Diagnostics,
display::Display,
outline::Outline
};
#[derive(Clone)]
struct AppState {