Compare commits

..

2 Commits

Author SHA1 Message Date
65cee1ecc2 Clean up the outline view (#19)
Clean up the source code and interface of the outline view. In addition, [fix a bug](commit/6e42681b719d7ec97c4225ca321225979bf87b56) that could cause `Assembly::realize` to react to itself under certain circumstances. Those circumstances arose, making the bug noticeable, while this branch was being written.

#### Source code

- Modularize the `Outline` component into smaller components.
- Switch from static iteration to dynamic Sycamore lists. This reduces the amount of re-rendering that happens when an element or constraint changes. It also allows constraint details to stay open or closed during constraint updates, rather than resetting to closed.
- Make `Element::index` private, as discussed [here](pulls/15#issuecomment-1816).

#### Interface

- Make constraints editable, updating the assembly realization on input. Flag constraints where the Lorentz product value doesn't parse.
- Round element vector coordinates to prevent the displayed strings from overlapping.

Note that issue #20 was created by this PR, but it will be addressed shortly.

Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Reviewed-on: #19
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2024-11-15 03:32:47 +00:00
707618cdd3 Integrate engine into application prototype (#15)
Port the engine prototype to Rust, integrate it into the application prototype, and use it to enforce the constraints.

### Features

To see the engine in action:

1. Add a constraint by shift-clicking to select two spheres in the outline view and then hitting the 🔗 button
2. Click a summary arrow to see the outline item for the new constraint
2. Set the constraint's Lorentz product by entering a value in the text field at the right end of the outline item
   * *The display should update as soon as you press* Enter *or focus away from the text field*

The checkbox at the left end of a constraint outline item controls whether the constraint is active. Activating a constraint triggers a solution update. (Deactivating a constraint doesn't, since the remaining active constraints are still satisfied.)

### Precision

The Julia prototype of the engine uses a generic scalar type, so you can pass in any type the linear algebra functions are implemented for. The examples use the [adjustable-precision](https://docs.julialang.org/en/v1/base/numbers/#Base.MPFR.setprecision) `BigFloat` type.

In the Rust port of the engine, the scalar type is currently fixed at `f64`. Switching to generic scalars shouldn't be too hard, but I haven't looked into [which other types](https://www.nalgebra.org/docs/user_guide/generic_programming) the linear algebra functions are implemented for.

### Testing

To confirm quantitatively that the Rust port of the engine is working, you can go to the `app-proto` folder and:

* Run some automated tests by calling `cargo test`.
* Inspect the optimization process in a few examples calling the `run-examples` script. The first example that prints is the same as the Irisawa hexlet example from the engine prototype. If you go into `engine-proto/gram-test`, launch Julia, and then

  ```
  include("irisawa-hexlet.jl")
  for (step, scaled_loss) in enumerate(history_alt.scaled_loss)
    println(rpad(step-1, 4), " | ", scaled_loss)
  end
  ```

  you should see that it prints basically the same loss history until the last few steps, when the lower default precision of the Rust engine really starts to show.

### A small engine revision

The Rust port of the engine improves on the Julia prototype in one part of the constraint-solving routine: projecting the Hessian onto the subspace where the frozen entries stay constant. The Julia prototype does this by removing the rows and columns of the Hessian that correspond to the frozen entries, finding the Newton step from the resulting "compressed" Hessian, and then adding zero entries to the Newton step in the appropriate places. The Rust port instead replaces each frozen row and column with its corresponding standard unit vector, avoiding the finicky compressing and decompressing steps.

To confirm that this version of the constraint-solving routine works the same as the original, I implemented it in Julia as `realize_gram_alt_proj`. The solutions we get from this routine match the ones we get from the original `realize_gram` to very high precision, and in the simplest examples (`sphere-in-tetrahedron.jl` and `tetrahedron-radius-ratio.jl`), the descent paths also match to very high precision. In a more complicated example (`irisawa-hexlet.jl`), the descent paths diverge about a quarter of the way into the search, even though they end up in the same place.

Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Reviewed-on: #15
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2024-11-12 00:46:16 +00:00
7 changed files with 404 additions and 328 deletions

View File

@ -1,7 +1,7 @@
[package]
name = "sketch-outline"
name = "dyna3"
version = "0.1.0"
authors = ["Aaron"]
authors = ["Aaron Fenyes", "Glen Whitney"]
edition = "2021"
[features]

View File

@ -2,8 +2,10 @@
<html>
<head>
<meta charset="utf-8"/>
<title>Sketch outline</title>
<title>dyna3</title>
<link data-trunk rel="css" href="main.css"/>
<link href="https://fonts.bunny.net/css?family=fira-sans:ital,wght@0,400;1,400&display=swap" rel="stylesheet">
<link href="https://fonts.bunny.net/css?family=noto-emoji:wght@400&text=%f0%9f%94%97%e2%9a%a0&display=swap" rel="stylesheet">
</head>
<body></body>
</html>

View File

@ -1,7 +1,20 @@
:root {
--text: #fcfcfc; /* almost white */
--text-bright: white;
--text-invalid: #f58fc2; /* bright pink */
--border: #555; /* light gray */
--border-focus: #aaa; /* bright gray */
--border-invalid: #70495c; /* dusky pink */
--selection-highlight: #444; /* medium gray */
--page-background: #222; /* dark gray */
--display-background: #020202; /* almost black */
}
body {
margin: 0px;
color: #fcfcfc;
background-color: #222;
color: var(--text);
background-color: var(--page-background);
font-family: 'Fira Sans', sans-serif;
}
/* sidebar */
@ -16,7 +29,7 @@ body {
padding: 0px;
border-width: 0px 1px 0px 0px;
border-style: solid;
border-color: #555;
border-color: var(--border);
}
/* add-remove */
@ -33,6 +46,15 @@ body {
font-size: large;
}
/* KLUDGE */
/*
for convenience, we're using emoji as temporary icons for some buttons. these
buttons need to be displayed in an emoji font
*/
#add-remove > button.emoji {
font-family: 'Noto Emoji', sans-serif;
}
/* outline */
#outline {
@ -51,81 +73,103 @@ summary {
}
summary.selected {
color: #fff;
background-color: #444;
color: var(--text-bright);
background-color: var(--selection-highlight);
}
summary > div, .cst {
summary > div, .constraint {
padding-top: 4px;
padding-bottom: 4px;
}
.elt, .cst {
.element, .constraint {
display: flex;
flex-grow: 1;
padding-left: 8px;
padding-right: 8px;
}
.elt-switch {
.element-switch {
width: 18px;
padding-left: 2px;
text-align: center;
}
details:has(li) .elt-switch::after {
details:has(li) .element-switch::after {
content: '▸';
}
details[open]:has(li) .elt-switch::after {
details[open]:has(li) .element-switch::after {
content: '▾';
}
.elt-label {
.element-label {
flex-grow: 1;
}
.cst-label {
.constraint-label {
flex-grow: 1;
}
.elt-rep {
.element-representation {
display: flex;
}
.elt-rep > div {
.element-representation > div {
padding: 2px 0px 0px 0px;
font-size: 10pt;
text-align: center;
font-variant-numeric: tabular-nums;
text-align: right;
width: 56px;
}
.cst {
.constraint {
font-style: italic;
}
.cst > input[type=checkbox] {
.constraint.invalid {
color: var(--text-invalid);
}
.constraint > input[type=checkbox] {
margin: 0px 8px 0px 0px;
}
.cst > input[type=text] {
color: #fcfcfc;
.constraint > input[type=text] {
color: inherit;
background-color: inherit;
border: 1px solid #555;
border: 1px solid var(--border);
border-radius: 2px;
}
.constraint.invalid > input[type=text] {
border-color: var(--border-invalid);
}
.status {
width: 20px;
padding-left: 4px;
text-align: center;
font-family: 'Noto Emoji';
font-style: normal;
}
.invalid > .status::after, details:has(.invalid):not([open]) .status::after {
content: '⚠';
color: var(--text-invalid);
}
/* display */
canvas {
float: left;
margin-left: 20px;
margin-top: 20px;
background-color: #020202;
border: 1px solid #555;
background-color: var(--display-background);
border: 1px solid var(--border);
border-radius: 16px;
}
canvas:focus {
border-color: #aaa;
border-color: var(--border-focus);
}

View File

@ -1,155 +1,130 @@
use std::collections::BTreeSet; /* DEBUG */
use sycamore::prelude::*;
use web_sys::{console, wasm_bindgen::JsValue};
use crate::{engine, AppState, assembly::{Assembly, Constraint, Element}};
/* 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(
Element {
id: String::from("gemini_a"),
label: String::from("Castor"),
color: [1.00_f32, 0.25_f32, 0.00_f32],
representation: engine::sphere(0.5, 0.5, 0.0, 1.0),
constraints: BTreeSet::default(),
index: 0
}
Element::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(
Element {
id: String::from("gemini_b"),
label: String::from("Pollux"),
color: [0.00_f32, 0.25_f32, 1.00_f32],
representation: engine::sphere(-0.5, -0.5, 0.0, 1.0),
constraints: BTreeSet::default(),
index: 0
}
Element::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(
Element {
id: String::from("ursa_major"),
label: String::from("Ursa major"),
color: [0.25_f32, 0.00_f32, 1.00_f32],
representation: engine::sphere(-0.5, 0.5, 0.0, 0.75),
constraints: BTreeSet::default(),
index: 0
}
Element::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(
Element {
id: String::from("ursa_minor"),
label: String::from("Ursa minor"),
color: [0.25_f32, 1.00_f32, 0.00_f32],
representation: engine::sphere(0.5, -0.5, 0.0, 0.5),
constraints: BTreeSet::default(),
index: 0
}
Element::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(
Element {
id: String::from("moon_deimos"),
label: String::from("Deimos"),
color: [0.75_f32, 0.75_f32, 0.00_f32],
representation: engine::sphere(0.0, 0.15, 1.0, 0.25),
constraints: BTreeSet::default(),
index: 0
}
Element::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(
Element {
id: String::from("moon_phobos"),
label: String::from("Phobos"),
color: [0.00_f32, 0.75_f32, 0.50_f32],
representation: engine::sphere(0.0, -0.15, -1.0, 0.25),
constraints: BTreeSet::default(),
index: 0
}
Element::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(
Element {
id: "central".to_string(),
label: "Central".to_string(),
color: [0.75_f32, 0.75_f32, 0.75_f32],
representation: engine::sphere(0.0, 0.0, 0.0, 1.0),
constraints: BTreeSet::default(),
index: 0
}
Element::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(
Element {
id: "assemb_plane".to_string(),
label: "Assembly plane".to_string(),
color: [0.75_f32, 0.75_f32, 0.75_f32],
representation: engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0),
constraints: BTreeSet::default(),
index: 0
}
Element::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(
Element {
id: "side1".to_string(),
label: "Side 1".to_string(),
color: [1.00_f32, 0.00_f32, 0.25_f32],
representation: engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0),
constraints: BTreeSet::default(),
index: 0
}
Element::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(
Element {
id: "side2".to_string(),
label: "Side 2".to_string(),
color: [0.25_f32, 1.00_f32, 0.00_f32],
representation: engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0),
constraints: BTreeSet::default(),
index: 0
}
Element::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(
Element {
id: "side3".to_string(),
label: "Side 3".to_string(),
color: [0.00_f32, 0.25_f32, 1.00_f32],
representation: engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0),
constraints: BTreeSet::default(),
index: 0
}
Element::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(
Element {
id: "corner1".to_string(),
label: "Corner 1".to_string(),
color: [0.75_f32, 0.75_f32, 0.75_f32],
representation: engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0),
constraints: BTreeSet::default(),
index: 0
}
Element::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(
Element {
id: "corner2".to_string(),
label: "Corner 2".to_string(),
color: [0.75_f32, 0.75_f32, 0.75_f32],
representation: engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0),
constraints: BTreeSet::default(),
index: 0
}
Element::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(
Element {
id: String::from("corner3"),
label: String::from("Corner 3"),
color: [0.75_f32, 0.75_f32, 0.75_f32],
representation: engine::sphere(2.0/3.0, 4.0/3.0 * a, 0.0, 1.0/3.0),
constraints: BTreeSet::default(),
index: 0
}
Element::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)
)
);
}
@ -202,6 +177,7 @@ pub fn AddRemove() -> View {
}
) { "+" }
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)
@ -215,15 +191,15 @@ pub fn AddRemove() -> View {
}
);
let lorentz_prod = create_signal(0.0);
let lorentz_prod_valid = create_signal(false);
let active = create_signal(true);
state.assembly.insert_constraint(Constraint {
subjects: subjects,
lorentz_prod: lorentz_prod,
lorentz_prod_text: create_signal(String::new()),
lorentz_prod_valid: create_signal(false),
lorentz_prod_valid: lorentz_prod_valid,
active: active,
});
state.assembly.realize();
state.selection.update(|sel| sel.clear());
/* DEBUG */
@ -241,23 +217,23 @@ pub fn AddRemove() -> View {
}
});
// update the realization when the constraint activated, or
// edited while active
// update the realization when the constraint becomes active
// and valid, or is edited while active and valid
create_effect(move || {
console::log_1(&JsValue::from(
format!("Constraint ({}, {}) updated", subjects.0, subjects.1)
));
lorentz_prod.track();
console::log_2(
&JsValue::from("Lorentz product updated to"),
&JsValue::from(lorentz_prod.get_untracked())
);
if active.get() {
if active.get() && lorentz_prod_valid.get() {
state.assembly.realize();
}
});
}
) { "🔗" }
select(bind:value=assembly_name) { /* DEBUG */
select(bind:value=assembly_name) { /* DEBUG */ // example assembly chooser
option(value="general") { "General" }
option(value="low-curv") { "Low-curvature" }
option(value="empty") { "Empty" }
}
}
}

View File

@ -18,17 +18,33 @@ pub struct Element {
pub id: String,
pub label: String,
pub color: ElementColor,
pub representation: DVector<f64>,
pub constraints: BTreeSet<ConstraintKey>,
pub representation: Signal<DVector<f64>>,
pub constraints: Signal<BTreeSet<ConstraintKey>>,
// the configuration matrix column index that was assigned to this element
// last time the assembly was realized
/* TO DO */
// this is public, as a kludge, because `Element` doesn't have a constructor
// yet. it should be made private as soon as the constructor is written
pub index: usize
column_index: usize
}
impl Element {
pub fn new(
id: String,
label: String,
color: ElementColor,
representation: DVector<f64>
) -> Element {
Element {
id: id,
label: label,
color: color,
representation: create_signal(representation),
constraints: create_signal(BTreeSet::default()),
column_index: 0
}
}
}
#[derive(Clone)]
pub struct Constraint {
pub subjects: (ElementKey, ElementKey),
@ -92,24 +108,23 @@ impl Assembly {
// create and insert a new element
self.insert_element_unchecked(
Element {
id: id,
label: format!("Sphere {}", id_num),
color: [0.75_f32, 0.75_f32, 0.75_f32],
representation: DVector::<f64>::from_column_slice(&[0.0, 0.0, 0.0, 0.5, -0.5]),
constraints: BTreeSet::default(),
index: 0
}
Element::new(
id,
format!("Sphere {}", id_num),
[0.75_f32, 0.75_f32, 0.75_f32],
DVector::<f64>::from_column_slice(&[0.0, 0.0, 0.0, 0.5, -0.5])
)
);
}
pub fn insert_constraint(&self, constraint: Constraint) {
let subjects = constraint.subjects;
let key = self.constraints.update(|csts| csts.insert(constraint));
self.elements.update(|elts| {
elts[subjects.0].constraints.insert(key);
elts[subjects.1].constraints.insert(key);
});
let subject_constraints = self.elements.with(
|elts| (elts[subjects.0].constraints, elts[subjects.1].constraints)
);
subject_constraints.0.update(|csts| csts.insert(key));
subject_constraints.1.update(|csts| csts.insert(key));
}
// --- realization ---
@ -118,7 +133,7 @@ impl Assembly {
// index the elements
self.elements.update_silent(|elts| {
for (index, (_, elt)) in elts.into_iter().enumerate() {
elt.index = index;
elt.column_index = index;
}
});
@ -130,8 +145,8 @@ impl Assembly {
for (_, cst) in csts {
if cst.active.get_untracked() && cst.lorentz_prod_valid.get_untracked() {
let subjects = cst.subjects;
let row = elts[subjects.0].index;
let col = elts[subjects.1].index;
let row = elts[subjects.0].column_index;
let col = elts[subjects.1].column_index;
gram_to_be.push_sym(row, col, cst.lorentz_prod.get_untracked());
}
}
@ -141,9 +156,9 @@ impl Assembly {
// Gram matrix
let mut guess_to_be = DMatrix::<f64>::zeros(5, elts.len());
for (_, elt) in elts {
let index = elt.index;
let index = elt.column_index;
gram_to_be.push_sym(index, index, 1.0);
guess_to_be.set_column(index, &elt.representation);
guess_to_be.set_column(index, &elt.representation.get_clone_untracked());
}
(gram_to_be, guess_to_be)
@ -185,11 +200,11 @@ impl Assembly {
if success {
// read out the solution
self.elements.update(|elts| {
for (_, elt) in elts.iter_mut() {
elt.representation.set_column(0, &config.column(elt.index));
for (_, elt) in self.elements.get_clone_untracked() {
elt.representation.update(
|rep| rep.set_column(0, &config.column(elt.column_index))
);
}
});
}
}
}

View File

@ -103,7 +103,11 @@ pub fn Display() -> View {
// change listener
let scene_changed = create_signal(true);
create_effect(move || {
state.assembly.elements.track();
state.assembly.elements.with(|elts| {
for (_, elt) in elts {
elt.representation.track();
}
});
state.selection.track();
scene_changed.set(true);
});
@ -295,25 +299,40 @@ pub fn Display() -> View {
let assembly_to_world = &location * &orientation;
// get the assembly
let elements = state.assembly.elements.get_clone();
let element_iter = (&elements).into_iter();
let reps_world: Vec<_> = element_iter.clone().map(
|(_, elt)| &assembly_to_world * &elt.representation
).collect();
let colors: Vec<_> = element_iter.clone().map(|(key, elt)|
let (
elt_cnt,
reps_world,
colors,
highlights
) = state.assembly.elements.with(|elts| {
(
// number of elements
elts.len() as i32,
// representation vectors in world coordinates
elts.iter().map(
|(_, elt)| elt.representation.with(|rep| &assembly_to_world * rep)
).collect::<Vec<_>>(),
// colors
elts.iter().map(|(key, elt)| {
if state.selection.with(|sel| sel.contains(&key)) {
elt.color.map(|ch| 0.2 + 0.8*ch)
} else {
elt.color
}
).collect();
let highlights: Vec<_> = element_iter.map(|(key, _)|
}).collect::<Vec<_>>(),
// highlight levels
elts.iter().map(|(key, _)| {
if state.selection.with(|sel| sel.contains(&key)) {
1.0_f32
} else {
HIGHLIGHT
}
).collect();
}).collect::<Vec<_>>()
)
});
// set the resolution
let width = canvas.width() as f32;
@ -322,7 +341,7 @@ pub fn Display() -> View {
ctx.uniform1f(shortdim_loc.as_ref(), width.min(height));
// pass the assembly
ctx.uniform1i(sphere_cnt_loc.as_ref(), elements.len() as i32);
ctx.uniform1i(sphere_cnt_loc.as_ref(), elt_cnt);
for n in 0..reps_world.len() {
let v = &reps_world[n];
ctx.uniform3f(

View File

@ -1,7 +1,6 @@
use itertools::Itertools;
use sycamore::{prelude::*, web::tags::div};
use sycamore::prelude::*;
use web_sys::{
Element,
Event,
HtmlInputElement,
KeyboardEvent,
@ -9,7 +8,7 @@ use web_sys::{
wasm_bindgen::JsCast
};
use crate::{AppState, assembly::Constraint};
use crate::{AppState, assembly, assembly::{Constraint, ConstraintKey, ElementKey}};
// an editable view of the Lorentz product representing a constraint
#[component(inline_props)]
@ -32,51 +31,48 @@ fn LorentzProductInput(constraint: Constraint) -> View {
}
}
// a component that lists the elements of the current assembly, showing the
// constraints on each element as a collapsible sub-list. its implementation
// is based on Kate Morley's HTML + CSS tree views:
//
// https://iamkate.com/code/tree-views/
//
#[component]
pub fn Outline() -> View {
// sort the elements alphabetically by ID
let elements_sorted = create_memo(|| {
// a list item that shows a constraint in an outline view of an element
#[component(inline_props)]
fn ConstraintOutlineItem(constraint_key: ConstraintKey, element_key: ElementKey) -> View {
let state = use_context::<AppState>();
state.assembly.elements
.get_clone()
.into_iter()
.sorted_by_key(|(_, elt)| elt.id.clone())
.collect()
});
view! {
ul(
id="outline",
on:click={
let state = use_context::<AppState>();
move |_| state.selection.update(|sel| sel.clear())
}
) {
Keyed(
list=elements_sorted,
view=|(key, elt)| {
let state = use_context::<AppState>();
let class = create_memo({
move || {
if state.selection.with(|sel| sel.contains(&key)) {
"selected"
let assembly = &state.assembly;
let constraint = assembly.constraints.with(|csts| csts[constraint_key].clone());
let other_subject = if constraint.subjects.0 == element_key {
constraint.subjects.1
} else {
""
constraint.subjects.0
};
let other_subject_label = assembly.elements.with(|elts| elts[other_subject].label.clone());
let class = constraint.lorentz_prod_valid.map(
|&lorentz_prod_valid| if lorentz_prod_valid { "constraint" } else { "constraint invalid" }
);
view! {
li(class=class.get()) {
input(r#type="checkbox", bind:checked=constraint.active)
div(class="constraint-label") { (other_subject_label) }
LorentzProductInput(constraint=constraint)
div(class="status")
}
}
});
let label = elt.label.clone();
let rep_components = elt.representation.iter().map(|u| {
let u_coord = u.to_string().replace("-", "\u{2212}");
View::from(div().children(u_coord))
}).collect::<Vec<_>>();
let constrained = elt.constraints.len() > 0;
}
// a list item that shows an element in an outline view of an assembly
#[component(inline_props)]
fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View {
let state = use_context::<AppState>();
let class = state.selection.map(
move |sel| if sel.contains(&key) { "selected" } else { "" }
);
let label = element.label.clone();
let rep_components = element.representation.map(
|rep| rep.iter().map(
|u| format!("{:.3}", u).replace("-", "\u{2212}")
).collect()
);
let constrained = element.constraints.map(|csts| csts.len() > 0);
let constraint_list = element.constraints.map(
|csts| csts.clone().into_iter().collect()
);
let details_node = create_node_ref();
view! {
li {
@ -101,16 +97,16 @@ pub fn Outline() -> View {
}
event.prevent_default();
},
"ArrowRight" if constrained => {
"ArrowRight" if constrained.get() => {
let _ = details_node
.get()
.unchecked_into::<Element>()
.unchecked_into::<web_sys::Element>()
.set_attribute("open", "");
},
"ArrowLeft" => {
let _ = details_node
.get()
.unchecked_into::<Element>()
.unchecked_into::<web_sys::Element>()
.remove_attribute("open");
},
_ => ()
@ -119,11 +115,11 @@ pub fn Outline() -> View {
}
) {
div(
class="elt-switch",
class="element-switch",
on:click=|event: MouseEvent| event.stop_propagation()
)
div(
class="elt",
class="element",
on:click={
move |event: MouseEvent| {
if event.shift_key() {
@ -143,44 +139,68 @@ pub fn Outline() -> View {
}
}
) {
div(class="elt-label") { (label) }
div(class="elt-rep") { (rep_components) }
div(class="element-label") { (label) }
div(class="element-representation") {
Indexed(
list=rep_components,
view=|coord_str| view! {
div { (coord_str) }
}
)
}
div(class="status")
}
}
ul(class="constraints") {
Keyed(
list=elt.constraints.into_iter().collect::<Vec<_>>(),
view=move |c_key| {
let c_state = use_context::<AppState>();
let assembly = &c_state.assembly;
let cst = assembly.constraints.with(|csts| csts[c_key].clone());
let other_arg = if cst.subjects.0 == key {
cst.subjects.1
} else {
cst.subjects.0
};
let other_arg_label = assembly.elements.with(|elts| elts[other_arg].label.clone());
list=constraint_list,
view=move |cst_key| view! {
ConstraintOutlineItem(
constraint_key=cst_key,
element_key=key
)
},
key=|cst_key| cst_key.clone()
)
}
}
}
}
}
// a component that lists the elements of the current assembly, showing the
// constraints on each element as a collapsible sub-list. its implementation
// is based on Kate Morley's HTML + CSS tree views:
//
// https://iamkate.com/code/tree-views/
//
#[component]
pub fn Outline() -> View {
let state = use_context::<AppState>();
// list the elements alphabetically by ID
let element_list = state.assembly.elements.map(
|elts| elts
.clone()
.into_iter()
.sorted_by_key(|(_, elt)| elt.id.clone())
.collect()
);
view! {
li(class="cst") {
input(r#type="checkbox", bind:checked=cst.active)
div(class="cst-label") { (other_arg_label) }
LorentzProductInput(constraint=cst)
}
ul(
id="outline",
on:click={
let state = use_context::<AppState>();
move |_| state.selection.update(|sel| sel.clear())
}
) {
Keyed(
list=element_list,
view=|(key, elt)| view! {
ElementOutlineItem(key=key, element=elt)
},
key=|c_key| c_key.clone()
)
}
}
}
}
},
key=|(key, elt)| (
key.clone(),
elt.id.clone(),
elt.label.clone(),
elt.constraints.clone()
)
key=|(key, _)| key.clone()
)
}
}