Compare commits

...
Sign in to create a new pull request.

10 commits

Author SHA1 Message Date
Aaron Fenyes
f9ff1c0f43 Update continuous integration image to Rust 1.86
All checks were successful
/ test (pull_request) Successful in 3m12s
2025-05-06 10:27:03 -07:00
Aaron Fenyes
3dd6303e99 Update assembly tests to use pointers
Some checks failed
/ test (pull_request) Failing after 13s
2025-05-05 21:26:02 -07:00
Aaron Fenyes
501cd74c96 Use B-tree collections instead of Fx hash ones
This removes a dependency. Since the collections in question should
usually be pretty small, it might also reduce memory overhead at little
cost in speed. Here are some relevant performance discussions:

  https://users.rust-lang.org/t/hashmap-vs-btreemap/13804
  https://www.reddit.com/r/rust/comments/7rgowj/hashmap_vs_btreemap/
2025-05-04 12:49:35 -07:00
Aaron Fenyes
c6b628d424 Store elements and regulators without keys
Since we're no longer using storage keys to refer to elements and
regulators, we don't need to store these items in keyed collections
anymore.

To keep element and regulator pointers in `BTreeSet` collections, we
implement `Ord` for `Serial` trait objects. As a bonus, this lets us
turn the element-wise regulator collections back into `BTreeSet`.
2025-05-04 12:32:37 -07:00
Aaron Fenyes
8a86038de0 Use pointers, not keys, to refer to regulators
In the process, move the code that used to handle serial numbering for
elements into the `Serial` trait, where it can provide serial numbers
for regulators too.
2025-05-04 10:59:28 -07:00
Aaron Fenyes
fbd6177a07 Drop unused context
Using pointers to refer to elements reduces the need to drag context
around.
2025-05-04 01:19:00 -07:00
Aaron Fenyes
5191534886 Use pointers for indexing elements by ID 2025-05-04 00:24:31 -07:00
Aaron Fenyes
ab01c26415 Use pointers, not keys, to refer to elements
Use reference-counted pointers, rather than collection keys, to refer to
elements in tasks like specifying the subjects of regulators, handling
selection, and creating interface components.
2025-05-04 00:09:30 -07:00
Aaron Fenyes
17f30d1312 Compare elements by serial number, not address
Rust pointers don't seem to be designed for use as identifiers, so it's
probably more idiomatic and possibly more reliable to manage our own
unique identifier system.

  https://stackoverflow.com/a/72149089

This also lets us get rid of the `ElementRc` newtype.
2025-05-03 00:08:17 -07:00
Aaron Fenyes
347981201c Use select for click selection in outline
This gets rid of duplicated code.
2025-05-02 14:51:51 -07:00
8 changed files with 288 additions and 270 deletions

View file

@ -11,7 +11,7 @@ jobs:
test:
runs-on: docker
container:
image: cimg/rust:1.85-node
image: cimg/rust:1.86-node
defaults:
run:
# set the default working directory for each `run` step, relative to the

19
app-proto/Cargo.lock generated
View file

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
version = 4
[[package]]
name = "ahash"
@ -89,8 +89,6 @@ dependencies = [
"lazy_static",
"nalgebra",
"readonly",
"rustc-hash",
"slab",
"sycamore",
"wasm-bindgen-test",
"web-sys",
@ -365,12 +363,6 @@ dependencies = [
"syn",
]
[[package]]
name = "rustc-hash"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152"
[[package]]
name = "safe_arch"
version = "0.7.2"
@ -414,15 +406,6 @@ dependencies = [
"wide",
]
[[package]]
name = "slab"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
dependencies = [
"autocfg",
]
[[package]]
name = "slotmap"
version = "1.0.7"

View file

@ -3,6 +3,7 @@ name = "dyna3"
version = "0.1.0"
authors = ["Aaron Fenyes", "Glen Whitney"]
edition = "2021"
rust-version = "1.86"
[features]
default = ["console_error_panic_hook"]
@ -14,8 +15,6 @@ js-sys = "0.3.70"
lazy_static = "1.5.0"
nalgebra = "0.33.0"
readonly = "0.2.12"
rustc-hash = "2.0.0"
slab = "0.4.9"
sycamore = "0.9.0-beta.3"
# The `console_error_panic_hook` crate provides better debugging of panics by

View file

@ -241,7 +241,7 @@ pub fn AddRemove() -> View {
.unwrap()
);
state.assembly.insert_regulator(
Rc::new(InversiveDistanceRegulator::new(subjects, &state.assembly))
Rc::new(InversiveDistanceRegulator::new(subjects))
);
state.selection.update(|sel| sel.clear());
}

View file

@ -1,12 +1,14 @@
use nalgebra::{DMatrix, DVector, DVectorView};
use rustc_hash::FxHashMap;
use slab::Slab;
use std::{
any::{Any, TypeId},
cell::Cell,
collections::BTreeSet,
collections::{BTreeMap, BTreeSet},
cmp::Ordering,
fmt,
fmt::{Debug, Formatter},
hash::{Hash, Hasher},
rc::Rc,
sync::atomic::{AtomicU64, Ordering}
sync::{atomic, atomic::AtomicU64}
};
use sycamore::prelude::*;
use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */
@ -27,49 +29,16 @@ use crate::{
specified::SpecifiedValue
};
// the types of the keys we use to access an assembly's elements and regulators
pub type ElementKey = usize;
pub type RegulatorKey = usize;
pub type ElementColor = [f32; 3];
/* KLUDGE */
// we should reconsider this design when we build a system for switching between
// assemblies. at that point, we might want to switch to hierarchical keys,
// where each each element has a key that identifies it within its assembly and
// where each each item has a key that identifies it within its assembly and
// each assembly has a key that identifies it within the sesssion
static NEXT_ELEMENT_SERIAL: AtomicU64 = AtomicU64::new(0);
static NEXT_SERIAL: AtomicU64 = AtomicU64::new(0);
pub trait ProblemPoser {
fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab<Rc<dyn Element>>);
}
pub trait Element: ProblemPoser + DisplayItem {
// the default identifier for an element of this type
fn default_id() -> String where Self: Sized;
// create the default example of an element of this type
fn default(id: String, id_num: u64) -> Self where Self: Sized;
// the regulators that should be created when an element of this type is
// inserted into the given assembly with the given storage key
/* KLUDGE */
// right now, this organization makes sense because regulators identify
// their subjects by storage key, so the element has to be inserted before
// its regulators can be created. if we change the way regulators identify
// their subjects, we should consider refactoring
fn default_regulators(_key: ElementKey, _assembly: &Assembly) -> Vec<Rc<dyn Regulator>> where Self: Sized {
Vec::new()
}
fn id(&self) -> &String;
fn label(&self) -> &String;
fn representation(&self) -> Signal<DVector<f64>>;
// the regulators the element is subject to. the assembly that owns the
// element is responsible for keeping this set up to date
fn regulators(&self) -> Signal<BTreeSet<RegulatorKey>>;
pub trait Serial {
// a serial number that uniquely identifies this element
fn serial(&self) -> u64;
@ -80,11 +49,62 @@ pub trait Element: ProblemPoser + DisplayItem {
//
// https://marabos.nl/atomics/atomics.html#example-handle-overflow
//
NEXT_ELEMENT_SERIAL.fetch_update(
Ordering::SeqCst, Ordering::SeqCst,
NEXT_SERIAL.fetch_update(
atomic::Ordering::SeqCst, atomic::Ordering::SeqCst,
|serial| serial.checked_add(1)
).expect("Out of serial numbers for elements")
}
}
impl Hash for dyn Serial {
fn hash<H: Hasher>(&self, state: &mut H) {
self.serial().hash(state)
}
}
impl PartialEq for dyn Serial {
fn eq(&self, other: &Self) -> bool {
self.serial() == other.serial()
}
}
impl Eq for dyn Serial {}
impl PartialOrd for dyn Serial {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for dyn Serial {
fn cmp(&self, other: &Self) -> Ordering {
self.serial().cmp(&other.serial())
}
}
pub trait ProblemPoser {
fn pose(&self, problem: &mut ConstraintProblem);
}
pub trait Element: Serial + ProblemPoser + DisplayItem {
// the default identifier for an element of this type
fn default_id() -> String where Self: Sized;
// the default example of an element of this type
fn default(id: String, id_num: u64) -> Self where Self: Sized;
// the default regulators that come with this element
fn default_regulators(self: Rc<Self>) -> Vec<Rc<dyn Regulator>> {
Vec::new()
}
fn id(&self) -> &String;
fn label(&self) -> &String;
fn representation(&self) -> Signal<DVector<f64>>;
// the regulators the element is subject to. the assembly that owns the
// element is responsible for keeping this set up to date
fn regulators(&self) -> Signal<BTreeSet<Rc<dyn Regulator>>>;
// the configuration matrix column index that was assigned to the element
// last time the assembly was realized, or `None` if the element has never
@ -97,17 +117,35 @@ pub trait Element: ProblemPoser + DisplayItem {
fn set_column_index(&self, index: usize);
}
// the `Element` trait needs to be dyn-compatible, so its method signatures can
// only use `Self` in the type of the receiver. that means `Element` can't
// implement `PartialEq`. if you need partial equivalence for `Element` trait
// objects, use this wrapper
#[derive(Clone)]
pub struct ElementRc(pub Rc<dyn Element>);
impl Debug for dyn Element {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
self.id().fmt(f)
}
}
impl PartialEq for ElementRc {
fn eq(&self, ElementRc(other): &Self) -> bool {
let ElementRc(rc) = self;
Rc::ptr_eq(rc, &other)
impl Hash for dyn Element {
fn hash<H: Hasher>(&self, state: &mut H) {
<dyn Serial>::hash(self, state)
}
}
impl PartialEq for dyn Element {
fn eq(&self, other: &Self) -> bool {
<dyn Serial>::eq(self, other)
}
}
impl Eq for dyn Element {}
impl PartialOrd for dyn Element {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
<dyn Serial>::partial_cmp(self, other)
}
}
impl Ord for dyn Element {
fn cmp(&self, other: &Self) -> Ordering {
<dyn Serial>::cmp(self, other)
}
}
@ -116,8 +154,8 @@ pub struct Sphere {
pub label: String,
pub color: ElementColor,
pub representation: Signal<DVector<f64>>,
pub regulators: Signal<BTreeSet<RegulatorKey>>,
pub serial: u64,
pub regulators: Signal<BTreeSet<Rc<dyn Regulator>>>,
serial: u64,
column_index: Cell<Option<usize>>
}
@ -135,7 +173,7 @@ impl Sphere {
label: label,
color: color,
representation: create_signal(representation),
regulators: create_signal(BTreeSet::default()),
regulators: create_signal(BTreeSet::new()),
serial: Self::next_serial(),
column_index: None.into()
}
@ -156,8 +194,8 @@ impl Element for Sphere {
)
}
fn default_regulators(key: ElementKey, assembly: &Assembly) -> Vec<Rc<dyn Regulator>> {
vec![Rc::new(HalfCurvatureRegulator::new(key, assembly))]
fn default_regulators(self: Rc<Self>) -> Vec<Rc<dyn Regulator>> {
vec![Rc::new(HalfCurvatureRegulator::new(self))]
}
fn id(&self) -> &String {
@ -172,14 +210,10 @@ impl Element for Sphere {
self.representation
}
fn regulators(&self) -> Signal<BTreeSet<RegulatorKey>> {
fn regulators(&self) -> Signal<BTreeSet<Rc<dyn Regulator>>> {
self.regulators
}
fn serial(&self) -> u64 {
self.serial
}
fn column_index(&self) -> Option<usize> {
self.column_index.get()
}
@ -189,8 +223,14 @@ impl Element for Sphere {
}
}
impl Serial for Sphere {
fn serial(&self) -> u64 {
self.serial
}
}
impl ProblemPoser for Sphere {
fn pose(&self, problem: &mut ConstraintProblem, _elts: &Slab<Rc<dyn Element>>) {
fn pose(&self, problem: &mut ConstraintProblem) {
let index = self.column_index().expect(
format!("Sphere \"{}\" should be indexed before writing problem data", self.id).as_str()
);
@ -204,8 +244,8 @@ pub struct Point {
pub label: String,
pub color: ElementColor,
pub representation: Signal<DVector<f64>>,
pub regulators: Signal<BTreeSet<RegulatorKey>>,
pub serial: u64,
pub regulators: Signal<BTreeSet<Rc<dyn Regulator>>>,
serial: u64,
column_index: Cell<Option<usize>>
}
@ -223,7 +263,7 @@ impl Point {
label,
color,
representation: create_signal(representation),
regulators: create_signal(BTreeSet::default()),
regulators: create_signal(BTreeSet::new()),
serial: Self::next_serial(),
column_index: None.into()
}
@ -256,14 +296,10 @@ impl Element for Point {
self.representation
}
fn regulators(&self) -> Signal<BTreeSet<RegulatorKey>> {
fn regulators(&self) -> Signal<BTreeSet<Rc<dyn Regulator>>> {
self.regulators
}
fn serial(&self) -> u64 {
self.serial
}
fn column_index(&self) -> Option<usize> {
self.column_index.get()
}
@ -273,8 +309,14 @@ impl Element for Point {
}
}
impl Serial for Point {
fn serial(&self) -> u64 {
self.serial
}
}
impl ProblemPoser for Point {
fn pose(&self, problem: &mut ConstraintProblem, _elts: &Slab<Rc<dyn Element>>) {
fn pose(&self, problem: &mut ConstraintProblem) {
let index = self.column_index().expect(
format!("Point \"{}\" should be indexed before writing problem data", self.id).as_str()
);
@ -284,8 +326,8 @@ impl ProblemPoser for Point {
}
}
pub trait Regulator: ProblemPoser + OutlineItem {
fn subjects(&self) -> Vec<ElementKey>;
pub trait Regulator: Serial + ProblemPoser + OutlineItem {
fn subjects(&self) -> Vec<Rc<dyn Element>>;
fn measurement(&self) -> ReadSignal<f64>;
fn set_point(&self) -> Signal<SpecifiedValue>;
@ -295,39 +337,65 @@ pub trait Regulator: ProblemPoser + OutlineItem {
// preconditioning when the set point is present, and use its return value
// to report whether the set is present. the default implementation does no
// preconditioning
fn try_activate(&self, _assembly: &Assembly) -> bool {
fn try_activate(&self) -> bool {
self.set_point().with(|set_pt| set_pt.is_present())
}
}
impl Hash for dyn Regulator {
fn hash<H: Hasher>(&self, state: &mut H) {
<dyn Serial>::hash(self, state)
}
}
impl PartialEq for dyn Regulator {
fn eq(&self, other: &Self) -> bool {
<dyn Serial>::eq(self, other)
}
}
impl Eq for dyn Regulator {}
impl PartialOrd for dyn Regulator {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
<dyn Serial>::partial_cmp(self, other)
}
}
impl Ord for dyn Regulator {
fn cmp(&self, other: &Self) -> Ordering {
<dyn Serial>::cmp(self, other)
}
}
pub struct InversiveDistanceRegulator {
pub subjects: [ElementKey; 2],
pub subjects: [Rc<dyn Element>; 2],
pub measurement: ReadSignal<f64>,
pub set_point: Signal<SpecifiedValue>
pub set_point: Signal<SpecifiedValue>,
serial: u64
}
impl InversiveDistanceRegulator {
pub fn new(subjects: [ElementKey; 2], assembly: &Assembly) -> InversiveDistanceRegulator {
let measurement = assembly.elements.map(
move |elts| {
let representations = subjects.map(|subj| elts[subj].representation());
representations[0].with(|rep_0|
representations[1].with(|rep_1|
rep_0.dot(&(&*Q * rep_1))
)
pub fn new(subjects: [Rc<dyn Element>; 2]) -> InversiveDistanceRegulator {
let representations = subjects.each_ref().map(|subj| subj.representation());
let measurement = create_memo(move || {
representations[0].with(|rep_0|
representations[1].with(|rep_1|
rep_0.dot(&(&*Q * rep_1))
)
}
);
)
});
let set_point = create_signal(SpecifiedValue::from_empty_spec());
let serial = Self::next_serial();
InversiveDistanceRegulator { subjects, measurement, set_point }
InversiveDistanceRegulator { subjects, measurement, set_point, serial }
}
}
impl Regulator for InversiveDistanceRegulator {
fn subjects(&self) -> Vec<ElementKey> {
self.subjects.into()
fn subjects(&self) -> Vec<Rc<dyn Element>> {
self.subjects.clone().into()
}
fn measurement(&self) -> ReadSignal<f64> {
@ -339,12 +407,18 @@ impl Regulator for InversiveDistanceRegulator {
}
}
impl Serial for InversiveDistanceRegulator {
fn serial(&self) -> u64 {
self.serial
}
}
impl ProblemPoser for InversiveDistanceRegulator {
fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab<Rc<dyn Element>>) {
fn pose(&self, problem: &mut ConstraintProblem) {
self.set_point.with_untracked(|set_pt| {
if let Some(val) = set_pt.value {
let [row, col] = self.subjects.map(
|subj| elts[subj].column_index().expect(
let [row, col] = self.subjects.each_ref().map(
|subj| subj.column_index().expect(
"Subjects should be indexed before inversive distance regulator writes problem data"
)
);
@ -355,28 +429,28 @@ impl ProblemPoser for InversiveDistanceRegulator {
}
pub struct HalfCurvatureRegulator {
pub subject: ElementKey,
pub subject: Rc<dyn Element>,
pub measurement: ReadSignal<f64>,
pub set_point: Signal<SpecifiedValue>
pub set_point: Signal<SpecifiedValue>,
serial: u64
}
impl HalfCurvatureRegulator {
pub fn new(subject: ElementKey, assembly: &Assembly) -> HalfCurvatureRegulator {
let measurement = assembly.elements.map(
move |elts| elts[subject].representation().with(
|rep| rep[Sphere::CURVATURE_COMPONENT]
)
pub fn new(subject: Rc<dyn Element>) -> HalfCurvatureRegulator {
let measurement = subject.representation().map(
|rep| rep[Sphere::CURVATURE_COMPONENT]
);
let set_point = create_signal(SpecifiedValue::from_empty_spec());
let serial = Self::next_serial();
HalfCurvatureRegulator { subject, measurement, set_point }
HalfCurvatureRegulator { subject, measurement, set_point, serial }
}
}
impl Regulator for HalfCurvatureRegulator {
fn subjects(&self) -> Vec<ElementKey> {
vec![self.subject]
fn subjects(&self) -> Vec<Rc<dyn Element>> {
vec![self.subject.clone()]
}
fn measurement(&self) -> ReadSignal<f64> {
@ -387,13 +461,10 @@ impl Regulator for HalfCurvatureRegulator {
self.set_point
}
fn try_activate(&self, assembly: &Assembly) -> bool {
fn try_activate(&self) -> bool {
match self.set_point.with(|set_pt| set_pt.value) {
Some(half_curv) => {
let representation = assembly.elements.with_untracked(
|elts| elts[self.subject].representation()
);
representation.update(
self.subject.representation().update(
|rep| change_half_curvature(rep, half_curv)
);
true
@ -403,11 +474,17 @@ impl Regulator for HalfCurvatureRegulator {
}
}
impl Serial for HalfCurvatureRegulator {
fn serial(&self) -> u64 {
self.serial
}
}
impl ProblemPoser for HalfCurvatureRegulator {
fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab<Rc<dyn Element>>) {
fn pose(&self, problem: &mut ConstraintProblem) {
self.set_point.with_untracked(|set_pt| {
if let Some(val) = set_pt.value {
let col = elts[self.subject].column_index().expect(
let col = self.subject.column_index().expect(
"Subject should be indexed before half-curvature regulator writes problem data"
);
problem.frozen.push(Sphere::CURVATURE_COMPONENT, col, val);
@ -418,7 +495,7 @@ impl ProblemPoser for HalfCurvatureRegulator {
// the velocity is expressed in uniform coordinates
pub struct ElementMotion<'a> {
pub key: ElementKey,
pub element: Rc<dyn Element>,
pub velocity: DVectorView<'a, f64>
}
@ -428,8 +505,8 @@ type AssemblyMotion<'a> = Vec<ElementMotion<'a>>;
#[derive(Clone)]
pub struct Assembly {
// elements and regulators
pub elements: Signal<Slab<Rc<dyn Element>>>,
pub regulators: Signal<Slab<Rc<dyn Regulator>>>,
pub elements: Signal<BTreeSet<Rc<dyn Element>>>,
pub regulators: Signal<BTreeSet<Rc<dyn Regulator>>>,
// solution variety tangent space. the basis vectors are stored in
// configuration matrix format, ordered according to the elements' column
@ -444,16 +521,16 @@ pub struct Assembly {
pub tangent: Signal<ConfigSubspace>,
// indexing
pub elements_by_id: Signal<FxHashMap<String, ElementKey>>
pub elements_by_id: Signal<BTreeMap<String, Rc<dyn Element>>>
}
impl Assembly {
pub fn new() -> Assembly {
Assembly {
elements: create_signal(Slab::new()),
regulators: create_signal(Slab::new()),
elements: create_signal(BTreeSet::new()),
regulators: create_signal(BTreeSet::new()),
tangent: create_signal(ConfigSubspace::zero(0)),
elements_by_id: create_signal(FxHashMap::default())
elements_by_id: create_signal(BTreeMap::default())
}
}
@ -462,29 +539,27 @@ impl Assembly {
// insert an element into the assembly without checking whether we already
// have an element with the same identifier. any element that does have the
// same identifier will get kicked out of the `elements_by_id` index
fn insert_element_unchecked<T: Element + 'static>(&self, elt: T) -> ElementKey {
fn insert_element_unchecked(&self, elt: impl Element + 'static) {
// insert the element
let id = elt.id().clone();
let key = self.elements.update(|elts| elts.insert(Rc::new(elt)));
self.elements_by_id.update(|elts_by_id| elts_by_id.insert(id, key));
let elt_rc = Rc::new(elt);
self.elements.update(|elts| elts.insert(elt_rc.clone()));
self.elements_by_id.update(|elts_by_id| elts_by_id.insert(id, elt_rc.clone()));
// create and insert the element's default regulators
for reg in T::default_regulators(key, &self) {
for reg in elt_rc.default_regulators() {
self.insert_regulator(reg);
}
key
}
pub fn try_insert_element(&self, elt: impl Element + 'static) -> Option<ElementKey> {
pub fn try_insert_element(&self, elt: impl Element + 'static) -> bool {
let can_insert = self.elements_by_id.with_untracked(
|elts_by_id| !elts_by_id.contains_key(elt.id())
);
if can_insert {
Some(self.insert_element_unchecked(elt))
} else {
None
self.insert_element_unchecked(elt);
}
can_insert
}
pub fn insert_element_default<T: Element + 'static>(&self) {
@ -505,19 +580,16 @@ impl Assembly {
pub fn insert_regulator(&self, regulator: Rc<dyn Regulator>) {
// add the regulator to the assembly's regulator list
let key = self.regulators.update(
self.regulators.update(
|regs| regs.insert(regulator.clone())
);
// add the regulator to each subject's regulator list
let subjects = regulator.subjects();
let subject_regulators: Vec<_> = self.elements.with_untracked(
|elts| subjects.into_iter().map(
|subj| elts[subj].regulators()
).collect()
);
let subject_regulators: Vec<_> = regulator.subjects().into_iter().map(
|subj| subj.regulators()
).collect();
for regulators in subject_regulators {
regulators.update(|regs| regs.insert(key));
regulators.update(|regs| regs.insert(regulator.clone()));
}
// update the realization when the regulator becomes a constraint, or is
@ -530,7 +602,7 @@ impl Assembly {
format!("Updated regulator with subjects {:?}", regulator.subjects())
));
if regulator.try_activate(&self_for_effect) {
if regulator.try_activate() {
self_for_effect.realize();
}
});
@ -539,7 +611,7 @@ impl Assembly {
// print an updated list of regulators
console::log_1(&JsValue::from("Regulators:"));
self.regulators.with_untracked(|regs| {
for (_, reg) in regs.into_iter() {
for reg in regs.into_iter() {
console::log_1(&JsValue::from(format!(
" {:?}: {}",
reg.subjects(),
@ -563,7 +635,7 @@ impl Assembly {
pub fn realize(&self) {
// index the elements
self.elements.update_silent(|elts| {
for (index, (_, elt)) in elts.into_iter().enumerate() {
for (index, elt) in elts.iter().enumerate() {
elt.set_column_index(index);
}
});
@ -571,12 +643,12 @@ impl Assembly {
// set up the constraint problem
let problem = self.elements.with_untracked(|elts| {
let mut problem = ConstraintProblem::new(elts.len());
for (_, elt) in elts {
elt.pose(&mut problem, elts);
for elt in elts {
elt.pose(&mut problem);
}
self.regulators.with_untracked(|regs| {
for (_, reg) in regs {
reg.pose(&mut problem, elts);
for reg in regs {
reg.pose(&mut problem);
}
});
problem
@ -618,7 +690,7 @@ impl Assembly {
if success {
// read out the solution
for (_, elt) in self.elements.get_clone_untracked() {
for elt in self.elements.get_clone_untracked() {
elt.representation().update(
|rep| rep.set_column(0, &config.column(elt.column_index().unwrap()))
);
@ -654,17 +726,17 @@ impl Assembly {
// in the process, we find out how many matrix columns we'll need to
// hold the deformation
let realized_dim = self.tangent.with(|tan| tan.assembly_dim());
let motion_dim = self.elements.update_silent(|elts| {
let motion_dim = {
let mut next_column_index = realized_dim;
for elt_motion in motion.iter() {
let moving_elt = &mut elts[elt_motion.key];
let moving_elt = &elt_motion.element;
if moving_elt.column_index().is_none() {
moving_elt.set_column_index(next_column_index);
next_column_index += 1;
}
}
next_column_index
});
};
// project the element motions onto the tangent space of the solution
// variety and sum them to get a deformation of the whole assembly. the
@ -675,9 +747,7 @@ impl Assembly {
for elt_motion in motion {
// we can unwrap the column index because we know that every moving
// element has one at this point
let column_index = self.elements.with_untracked(
|elts| elts[elt_motion.key].column_index().unwrap()
);
let column_index = elt_motion.element.column_index().unwrap();
if column_index < realized_dim {
// this element had a column index when we started, so by
@ -690,12 +760,8 @@ impl Assembly {
// this element didn't have a column index when we started, so
// by invariant (2), it's unconstrained
let mut target_column = motion_proj.column_mut(column_index);
let unif_to_std = self.elements.with_untracked(
|elts| {
elts[elt_motion.key].representation().with_untracked(
|rep| local_unif_to_std(rep.as_view())
)
}
let unif_to_std = elt_motion.element.representation().with_untracked(
|rep| local_unif_to_std(rep.as_view())
);
target_column += unif_to_std * elt_motion.velocity;
}
@ -705,7 +771,7 @@ impl Assembly {
// normalizations, so we restore those afterward
/* KLUDGE */
// for now, we only restore the normalizations of spheres
for (_, elt) in self.elements.get_clone_untracked() {
for elt in self.elements.get_clone_untracked() {
elt.representation().update_silent(|rep| {
match elt.column_index() {
Some(column_index) => {
@ -747,7 +813,7 @@ mod tests {
fn unindexed_element_test() {
let _ = create_root(|| {
let elt = Sphere::default("sphere".to_string(), 0);
elt.pose(&mut ConstraintProblem::new(1), &Slab::new());
elt.pose(&mut ConstraintProblem::new(1));
});
}
@ -755,18 +821,16 @@ mod tests {
#[should_panic(expected = "Subjects should be indexed before inversive distance regulator writes problem data")]
fn unindexed_subject_test_inversive_distance() {
let _ = create_root(|| {
let mut elts = Slab::<Rc<dyn Element>>::new();
let subjects = [0, 1].map(|k| {
elts.insert(
Rc::new(Sphere::default(format!("sphere{k}"), k))
)
});
elts[subjects[0]].set_column_index(0);
let subjects = [0, 1].map(
|k| Rc::new(Sphere::default(format!("sphere{k}"), k)) as Rc<dyn Element>
);
subjects[0].set_column_index(0);
InversiveDistanceRegulator {
subjects: subjects,
measurement: create_memo(|| 0.0),
set_point: create_signal(SpecifiedValue::try_from("0.0".to_string()).unwrap())
}.pose(&mut ConstraintProblem::new(2), &elts);
set_point: create_signal(SpecifiedValue::try_from("0.0".to_string()).unwrap()),
serial: InversiveDistanceRegulator::next_serial()
}.pose(&mut ConstraintProblem::new(2));
});
}
}

View file

@ -1,5 +1,6 @@
use core::array;
use nalgebra::{DMatrix, DVector, Rotation3, Vector3};
use std::rc::Rc;
use sycamore::{prelude::*, motion::create_raf};
use web_sys::{
console,
@ -16,7 +17,7 @@ use web_sys::{
use crate::{
AppState,
assembly::{ElementKey, ElementColor, ElementMotion, Point, Sphere}
assembly::{Element, ElementColor, ElementMotion, Point, Sphere}
};
// --- scene data ---
@ -362,7 +363,7 @@ pub fn Display() -> View {
let scene_changed = create_signal(true);
create_effect(move || {
state.assembly.elements.with(|elts| {
for (_, elt) in elts {
for elt in elts {
elt.representation().track();
}
});
@ -548,7 +549,7 @@ pub fn Display() -> View {
// manipulate the assembly
if state.selection.with(|sel| sel.len() == 1) {
let sel = state.selection.with(
|sel| *sel.into_iter().next().unwrap()
|sel| sel.into_iter().next().unwrap().clone()
);
let translate_x = translate_pos_x_val - translate_neg_x_val;
let translate_y = translate_pos_y_val - translate_neg_y_val;
@ -574,7 +575,7 @@ pub fn Display() -> View {
assembly_for_raf.deform(
vec![
ElementMotion {
key: sel,
element: sel,
velocity: elt_motion.as_view()
}
]
@ -615,8 +616,8 @@ pub fn Display() -> View {
// set up the scene
state.assembly.elements.with_untracked(
|elts| for (key, elt) in elts {
let selected = state.selection.with(|sel| sel.contains(&key));
|elts| for elt in elts {
let selected = state.selection.with(|sel| sel.contains(elt));
elt.show(&mut scene, selected);
}
);
@ -849,16 +850,16 @@ pub fn Display() -> View {
// find the nearest element along the pointer direction
let (dir, pixel_size) = event_dir(&event);
console::log_1(&JsValue::from(dir.to_string()));
let mut clicked: Option<(ElementKey, f64)> = None;
for (key, elt) in state.assembly.elements.get_clone_untracked() {
let mut clicked: Option<(Rc<dyn Element>, f64)> = None;
for elt in state.assembly.elements.get_clone_untracked() {
match assembly_to_world.with(|asm_to_world| elt.cast(dir, asm_to_world, pixel_size)) {
Some(depth) => match clicked {
Some((_, best_depth)) => {
if depth < best_depth {
clicked = Some((key, depth))
clicked = Some((elt, depth))
}
},
None => clicked = Some((key, depth))
None => clicked = Some((elt, depth))
}
None => ()
};
@ -866,7 +867,7 @@ pub fn Display() -> View {
// if we clicked something, select it
match clicked {
Some((key, _)) => state.select(key, event.shift_key()),
Some((elt, _)) => state.select(&elt, event.shift_key()),
None => state.selection.update(|sel| sel.clear())
};
}

View file

@ -8,42 +8,41 @@ mod specified;
#[cfg(test)]
mod tests;
use rustc_hash::FxHashSet;
use std::{collections::BTreeSet, rc::Rc};
use sycamore::prelude::*;
use add_remove::AddRemove;
use assembly::{Assembly, ElementKey};
use assembly::{Assembly, Element};
use display::Display;
use outline::Outline;
#[derive(Clone)]
struct AppState {
assembly: Assembly,
selection: Signal<FxHashSet<ElementKey>>
selection: Signal<BTreeSet<Rc<dyn Element>>>
}
impl AppState {
fn new() -> AppState {
AppState {
assembly: Assembly::new(),
selection: create_signal(FxHashSet::default())
selection: create_signal(BTreeSet::default())
}
}
// in single-selection mode, select the element with the given key. in
// multiple-selection mode, toggle whether the element with the given key
// is selected
fn select(&self, key: ElementKey, multi: bool) {
// in single-selection mode, select the given element. in multiple-selection
// mode, toggle whether the given element is selected
fn select(&self, element: &Rc<dyn Element>, multi: bool) {
if multi {
self.selection.update(|sel| {
if !sel.remove(&key) {
sel.insert(key);
if !sel.remove(element) {
sel.insert(element.clone());
}
});
} else {
self.selection.update(|sel| {
sel.clear();
sel.insert(key);
sel.insert(element.clone());
});
}
}

View file

@ -11,12 +11,9 @@ use crate::{
AppState,
assembly::{
Element,
ElementKey,
ElementRc,
HalfCurvatureRegulator,
InversiveDistanceRegulator,
Regulator,
RegulatorKey
Regulator
},
specified::SpecifiedValue
};
@ -92,20 +89,16 @@ fn RegulatorInput(regulator: Rc<dyn Regulator>) -> View {
}
pub trait OutlineItem {
fn outline_item(self: Rc<Self>, element_key: ElementKey) -> View;
fn outline_item(self: Rc<Self>, element: &Rc<dyn Element>) -> View;
}
impl OutlineItem for InversiveDistanceRegulator {
fn outline_item(self: Rc<Self>, element_key: ElementKey) -> View {
let state = use_context::<AppState>();
let other_subject = if self.subjects[0] == element_key {
self.subjects[1]
fn outline_item(self: Rc<Self>, element: &Rc<dyn Element>) -> View {
let other_subject_label = if self.subjects[0] == element.clone() {
self.subjects[1].label()
} else {
self.subjects[0]
};
let other_subject_label = state.assembly.elements.with(
|elts| elts[other_subject].label().clone()
);
self.subjects[0].label()
}.clone();
view! {
li(class="regulator") {
div(class="regulator-label") { (other_subject_label) }
@ -118,7 +111,7 @@ impl OutlineItem for InversiveDistanceRegulator {
}
impl OutlineItem for HalfCurvatureRegulator {
fn outline_item(self: Rc<Self>, _element_key: ElementKey) -> View {
fn outline_item(self: Rc<Self>, _element: &Rc<dyn Element>) -> View {
view! {
li(class="regulator") {
div(class="regulator-label") // for spacing
@ -130,23 +123,16 @@ impl OutlineItem for HalfCurvatureRegulator {
}
}
// a list item that shows a regulator in an outline view of an element
#[component(inline_props)]
fn RegulatorOutlineItem(regulator_key: RegulatorKey, element_key: ElementKey) -> View {
let state = use_context::<AppState>();
let regulator = state.assembly.regulators.with(
|regs| regs[regulator_key].clone()
);
regulator.outline_item(element_key)
}
// a list item that shows an element in an outline view of an assembly
#[component(inline_props)]
fn ElementOutlineItem(key: ElementKey, element: Rc<dyn Element>) -> View {
fn ElementOutlineItem(element: Rc<dyn Element>) -> View {
let state = use_context::<AppState>();
let class = state.selection.map(
move |sel| if sel.contains(&key) { "selected" } else { "" }
);
let class = {
let element_for_class = element.clone();
state.selection.map(
move |sel| if sel.contains(&element_for_class) { "selected" } else { "" }
)
};
let label = element.label().clone();
let representation = element.representation().clone();
let rep_components = move || {
@ -161,14 +147,10 @@ fn ElementOutlineItem(key: ElementKey, element: Rc<dyn Element>) -> View {
};
let regulated = element.regulators().map(|regs| regs.len() > 0);
let regulator_list = element.regulators().map(
move |elt_reg_keys| elt_reg_keys
|regs| regs
.clone()
.into_iter()
.sorted_by_key(
|&reg_key| state.assembly.regulators.with(
|regs| regs[reg_key].subjects().len()
)
)
.sorted_by_key(|reg| reg.subjects().len())
.collect()
);
let details_node = create_node_ref();
@ -178,10 +160,11 @@ fn ElementOutlineItem(key: ElementKey, element: Rc<dyn Element>) -> View {
summary(
class=class.get(),
on:keydown={
let element_for_handler = element.clone();
move |event: KeyboardEvent| {
match event.key().as_str() {
"Enter" => {
state.select(key, event.shift_key());
state.select(&element_for_handler, event.shift_key());
event.prevent_default();
},
"ArrowRight" if regulated.get() => {
@ -208,19 +191,10 @@ fn ElementOutlineItem(key: ElementKey, element: Rc<dyn Element>) -> View {
div(
class="element",
on:click={
let state_for_handler = state.clone();
let element_for_handler = element.clone();
move |event: MouseEvent| {
if event.shift_key() {
state.selection.update(|sel| {
if !sel.remove(&key) {
sel.insert(key);
}
});
} else {
state.selection.update(|sel| {
sel.clear();
sel.insert(key);
});
}
state_for_handler.select(&element_for_handler, event.shift_key());
event.stop_propagation();
event.prevent_default();
}
@ -234,13 +208,8 @@ fn ElementOutlineItem(key: ElementKey, element: Rc<dyn Element>) -> View {
ul(class="regulators") {
Keyed(
list=regulator_list,
view=move |reg_key| view! {
RegulatorOutlineItem(
regulator_key=reg_key,
element_key=key
)
},
key=|reg_key| reg_key.clone()
view=move |reg| reg.outline_item(&element),
key=|reg| reg.serial()
)
}
}
@ -259,12 +228,15 @@ pub fn Outline() -> View {
let state = use_context::<AppState>();
// list the elements alphabetically by ID
/* TO DO */
// this code is designed to generalize easily to other sort keys. if we only
// ever wanted to sort by ID, we could do that more simply using the
// `elements_by_id` index
let element_list = state.assembly.elements.map(
|elts| elts
.clone()
.into_iter()
.sorted_by_key(|(_, elt)| elt.id().clone())
.map(|(key, elt)| (key, ElementRc(elt)))
.sorted_by_key(|elt| elt.id().clone())
.collect()
);
@ -278,10 +250,10 @@ pub fn Outline() -> View {
) {
Keyed(
list=element_list,
view=|(key, ElementRc(elt))| view! {
ElementOutlineItem(key=key, element=elt)
view=|elt| view! {
ElementOutlineItem(element=elt)
},
key=|(_, ElementRc(elt))| elt.serial()
key=|elt| elt.serial()
)
}
}