Compare commits

..

24 Commits

Author SHA1 Message Date
Aaron Fenyes
5332fda6e4 Move new test to avoid merge conflict 2024-11-11 15:41:42 -08:00
Aaron Fenyes
22a93bee28 Confirm that frozen entries are frozen exactly 2024-11-11 15:34:51 -08:00
Aaron Fenyes
5839882ed7 Name constraint Lorentz product more descriptively 2024-11-11 00:27:28 -08:00
Aaron Fenyes
a4ec52a4e7 Alias the type of an element's color 2024-11-11 00:04:48 -08:00
abb9d35335 Correct align environment in notes 2024-11-11 07:56:27 +00:00
Aaron Fenyes
a170492e3d Add engine conventions to inversive coordinates notes 2024-11-10 23:47:34 -08:00
Aaron Fenyes
b8ca1139d5 Explain what the Element::index field holds
Also, remind us to make the field private when that becomes possible.
2024-11-10 23:22:30 -08:00
Aaron Fenyes
ced001bbfe Alias the types of element and constraint keys
This will make it easier to change the key types if we change how we
store and access elements and constraints.
2024-11-10 22:55:58 -08:00
Aaron Fenyes
ed1890bffc Improve naming of constraint subjects 2024-11-10 19:36:40 -08:00
Aaron Fenyes
da008fd090 Write out representation in Element structure 2024-11-10 19:24:26 -08:00
Aaron Fenyes
933f05661d Only compile engine::point when it's used
This function will eventually be used in the application, but right now
it's only used in tests.
2024-11-10 16:31:29 -08:00
Aaron Fenyes
9c191ae586 Polish log messages 2024-10-30 00:27:16 -07:00
Aaron Fenyes
9e31037e17 Spread web-sys imports over multiple lines 2024-10-30 00:19:44 -07:00
Aaron Fenyes
c2e3c64d4a Remove debug log from Lorentz product input 2024-10-30 00:16:34 -07:00
Aaron Fenyes
76ad4245d5 Factor out Lorentz product input 2024-10-29 23:43:41 -07:00
Aaron Fenyes
a46ef2c8d6 Work around data binding bug in number input
Setting `bind:value` or `bind:valueAsNumber` for a number input seems to
restrict what you can type in it. We work around this by switching to
text inputs for now. We should probably switch back to number inputs if
we can, though, because they let us take advantage of the browser's
parsing and validation.
2024-10-29 22:53:48 -07:00
Aaron Fenyes
e0880d2ad2 Make constraints editable 2024-10-29 22:32:00 -07:00
Aaron Fenyes
e5f4d523f9 Update the realization when a constraint is activated
Sycamore probably has a better way to do this, but this way works for
now.
2024-10-29 13:46:15 -07:00
Aaron Fenyes
a37c71153d Enforce constraints in the editor 2024-10-26 23:51:27 -07:00
Aaron Fenyes
ce33bbf418 Record optimization history 2024-10-26 01:07:17 -07:00
Aaron Fenyes
9f8632efb3 Port the Irisawa hexlet test to Rust
In the process, notice that the tolerance scale adjustment was ported
wrong, and correct it.
2024-10-25 21:43:53 -07:00
Aaron Fenyes
9fe03264ab Port the Gram matrix realization routine to Rust
Validate with the process inspection example tests, which print out
their results and optimization histories when run one at a time in
`--nocapture` mode.
2024-10-25 17:34:29 -07:00
Aaron Fenyes
e59d60bf77 Reorganize search state; remove unused variables 2024-10-25 17:17:49 -07:00
Aaron Fenyes
16df161fe7 Test alternate projection technique 2024-10-24 19:51:10 -07:00
7 changed files with 328 additions and 404 deletions

View File

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

View File

@ -2,10 +2,8 @@
<html>
<head>
<meta charset="utf-8"/>
<title>dyna3</title>
<title>Sketch outline</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,20 +1,7 @@
: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: var(--text);
background-color: var(--page-background);
font-family: 'Fira Sans', sans-serif;
color: #fcfcfc;
background-color: #222;
}
/* sidebar */
@ -29,7 +16,7 @@ body {
padding: 0px;
border-width: 0px 1px 0px 0px;
border-style: solid;
border-color: var(--border);
border-color: #555;
}
/* add-remove */
@ -46,15 +33,6 @@ 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 {
@ -73,103 +51,81 @@ summary {
}
summary.selected {
color: var(--text-bright);
background-color: var(--selection-highlight);
color: #fff;
background-color: #444;
}
summary > div, .constraint {
summary > div, .cst {
padding-top: 4px;
padding-bottom: 4px;
}
.element, .constraint {
.elt, .cst {
display: flex;
flex-grow: 1;
padding-left: 8px;
padding-right: 8px;
}
.element-switch {
.elt-switch {
width: 18px;
padding-left: 2px;
text-align: center;
}
details:has(li) .element-switch::after {
details:has(li) .elt-switch::after {
content: '▸';
}
details[open]:has(li) .element-switch::after {
details[open]:has(li) .elt-switch::after {
content: '▾';
}
.element-label {
.elt-label {
flex-grow: 1;
}
.constraint-label {
.cst-label {
flex-grow: 1;
}
.element-representation {
.elt-rep {
display: flex;
}
.element-representation > div {
.elt-rep > div {
padding: 2px 0px 0px 0px;
font-size: 10pt;
font-variant-numeric: tabular-nums;
text-align: right;
text-align: center;
width: 56px;
}
.constraint {
.cst {
font-style: italic;
}
.constraint.invalid {
color: var(--text-invalid);
}
.constraint > input[type=checkbox] {
.cst > input[type=checkbox] {
margin: 0px 8px 0px 0px;
}
.constraint > input[type=text] {
color: inherit;
.cst > input[type=text] {
color: #fcfcfc;
background-color: inherit;
border: 1px solid var(--border);
border: 1px solid #555;
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: var(--display-background);
border: 1px solid var(--border);
background-color: #020202;
border: 1px solid #555;
border-radius: 16px;
}
canvas:focus {
border-color: var(--border-focus);
border-color: #aaa;
}

View File

@ -1,130 +1,155 @@
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::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)
)
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
}
);
let _ = assembly.try_insert_element(
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)
)
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
}
);
let _ = assembly.try_insert_element(
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)
)
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
}
);
let _ = assembly.try_insert_element(
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)
)
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
}
);
let _ = assembly.try_insert_element(
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)
)
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
}
);
let _ = assembly.try_insert_element(
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)
)
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
}
);
}
/* 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::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)
)
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
}
);
let _ = assembly.try_insert_element(
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)
)
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
}
);
let _ = assembly.try_insert_element(
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)
)
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
}
);
let _ = assembly.try_insert_element(
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)
)
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
}
);
let _ = assembly.try_insert_element(
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)
)
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
}
);
let _ = assembly.try_insert_element(
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)
)
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
}
);
let _ = assembly.try_insert_element(
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)
)
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
}
);
let _ = assembly.try_insert_element(
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)
)
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
}
);
}
@ -177,7 +202,6 @@ 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)
@ -191,15 +215,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: lorentz_prod_valid,
lorentz_prod_valid: create_signal(false),
active: active,
});
state.assembly.realize();
state.selection.update(|sel| sel.clear());
/* DEBUG */
@ -217,23 +241,23 @@ pub fn AddRemove() -> View {
}
});
// update the realization when the constraint becomes active
// and valid, or is edited while active and valid
// update the realization when the constraint activated, or
// edited while active
create_effect(move || {
console::log_1(&JsValue::from(
format!("Constraint ({}, {}) updated", subjects.0, subjects.1)
));
lorentz_prod.track();
if active.get() && lorentz_prod_valid.get() {
console::log_2(
&JsValue::from("Lorentz product updated to"),
&JsValue::from(lorentz_prod.get_untracked())
);
if active.get() {
state.assembly.realize();
}
});
}
) { "🔗" }
select(bind:value=assembly_name) { /* DEBUG */ // example assembly chooser
select(bind:value=assembly_name) { /* DEBUG */
option(value="general") { "General" }
option(value="low-curv") { "Low-curvature" }
option(value="empty") { "Empty" }
}
}
}

View File

@ -18,33 +18,17 @@ pub struct Element {
pub id: String,
pub label: String,
pub color: ElementColor,
pub representation: Signal<DVector<f64>>,
pub constraints: Signal<BTreeSet<ConstraintKey>>,
pub representation: DVector<f64>,
pub constraints: BTreeSet<ConstraintKey>,
// the configuration matrix column index that was assigned to this element
// last time the assembly was realized
column_index: usize
/* 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
}
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),
@ -108,23 +92,24 @@ impl Assembly {
// create and insert a new element
self.insert_element_unchecked(
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])
)
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
}
);
}
pub fn insert_constraint(&self, constraint: Constraint) {
let subjects = constraint.subjects;
let key = self.constraints.update(|csts| csts.insert(constraint));
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));
self.elements.update(|elts| {
elts[subjects.0].constraints.insert(key);
elts[subjects.1].constraints.insert(key);
});
}
// --- realization ---
@ -133,7 +118,7 @@ impl Assembly {
// index the elements
self.elements.update_silent(|elts| {
for (index, (_, elt)) in elts.into_iter().enumerate() {
elt.column_index = index;
elt.index = index;
}
});
@ -145,8 +130,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].column_index;
let col = elts[subjects.1].column_index;
let row = elts[subjects.0].index;
let col = elts[subjects.1].index;
gram_to_be.push_sym(row, col, cst.lorentz_prod.get_untracked());
}
}
@ -156,9 +141,9 @@ impl Assembly {
// Gram matrix
let mut guess_to_be = DMatrix::<f64>::zeros(5, elts.len());
for (_, elt) in elts {
let index = elt.column_index;
let index = elt.index;
gram_to_be.push_sym(index, index, 1.0);
guess_to_be.set_column(index, &elt.representation.get_clone_untracked());
guess_to_be.set_column(index, &elt.representation);
}
(gram_to_be, guess_to_be)
@ -200,11 +185,11 @@ impl Assembly {
if success {
// read out the solution
for (_, elt) in self.elements.get_clone_untracked() {
elt.representation.update(
|rep| rep.set_column(0, &config.column(elt.column_index))
);
}
self.elements.update(|elts| {
for (_, elt) in elts.iter_mut() {
elt.representation.set_column(0, &config.column(elt.index));
}
});
}
}
}

View File

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

View File

@ -1,6 +1,7 @@
use itertools::Itertools;
use sycamore::prelude::*;
use sycamore::{prelude::*, web::tags::div};
use web_sys::{
Element,
Event,
HtmlInputElement,
KeyboardEvent,
@ -8,7 +9,7 @@ use web_sys::{
wasm_bindgen::JsCast
};
use crate::{AppState, assembly, assembly::{Constraint, ConstraintKey, ElementKey}};
use crate::{AppState, assembly::Constraint};
// an editable view of the Lorentz product representing a constraint
#[component(inline_props)]
@ -31,143 +32,6 @@ fn LorentzProductInput(constraint: Constraint) -> View {
}
}
// 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>();
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")
}
}
}
// 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 {
details(ref=details_node) {
summary(
class=class.get(),
on:keydown={
move |event: KeyboardEvent| {
match event.key().as_str() {
"Enter" => {
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);
});
}
event.prevent_default();
},
"ArrowRight" if constrained.get() => {
let _ = details_node
.get()
.unchecked_into::<web_sys::Element>()
.set_attribute("open", "");
},
"ArrowLeft" => {
let _ = details_node
.get()
.unchecked_into::<web_sys::Element>()
.remove_attribute("open");
},
_ => ()
}
}
}
) {
div(
class="element-switch",
on:click=|event: MouseEvent| event.stop_propagation()
)
div(
class="element",
on:click={
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);
});
}
event.stop_propagation();
event.prevent_default();
}
}
) {
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=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:
@ -176,16 +40,15 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View {
//
#[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()
// sort the elements alphabetically by ID
let elements_sorted = create_memo(|| {
let state = use_context::<AppState>();
state.assembly.elements
.get_clone()
.into_iter()
.sorted_by_key(|(_, elt)| elt.id.clone())
.collect()
);
});
view! {
ul(
@ -196,11 +59,128 @@ pub fn Outline() -> View {
}
) {
Keyed(
list=element_list,
view=|(key, elt)| view! {
ElementOutlineItem(key=key, element=elt)
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"
} else {
""
}
}
});
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;
let details_node = create_node_ref();
view! {
li {
details(ref=details_node) {
summary(
class=class.get(),
on:keydown={
move |event: KeyboardEvent| {
match event.key().as_str() {
"Enter" => {
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);
});
}
event.prevent_default();
},
"ArrowRight" if constrained => {
let _ = details_node
.get()
.unchecked_into::<Element>()
.set_attribute("open", "");
},
"ArrowLeft" => {
let _ = details_node
.get()
.unchecked_into::<Element>()
.remove_attribute("open");
},
_ => ()
}
}
}
) {
div(
class="elt-switch",
on:click=|event: MouseEvent| event.stop_propagation()
)
div(
class="elt",
on:click={
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);
});
}
event.stop_propagation();
event.prevent_default();
}
}
) {
div(class="elt-label") { (label) }
div(class="elt-rep") { (rep_components) }
}
}
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());
view! {
li(class="cst") {
input(r#type="checkbox", bind:checked=cst.active)
div(class="cst-label") { (other_arg_label) }
LorentzProductInput(constraint=cst)
}
}
},
key=|c_key| c_key.clone()
)
}
}
}
}
},
key=|(key, _)| key.clone()
key=|(key, elt)| (
key.clone(),
elt.id.clone(),
elt.label.clone(),
elt.constraints.clone()
)
)
}
}