Clean up the outline view #19

Merged
glen merged 23 commits from outline-cleanup_on_main into main 2024-11-15 03:32:48 +00:00
7 changed files with 404 additions and 328 deletions

View File

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

View File

@ -2,8 +2,10 @@
<html> <html>
<head> <head>
<meta charset="utf-8"/> <meta charset="utf-8"/>
<title>Sketch outline</title> <title>dyna3</title>
<link data-trunk rel="css" href="main.css"/> <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> </head>
<body></body> <body></body>
</html> </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 { body {
margin: 0px; margin: 0px;
color: #fcfcfc; color: var(--text);
background-color: #222; background-color: var(--page-background);
font-family: 'Fira Sans', sans-serif;
} }
/* sidebar */ /* sidebar */
@ -16,7 +29,7 @@ body {
padding: 0px; padding: 0px;
border-width: 0px 1px 0px 0px; border-width: 0px 1px 0px 0px;
border-style: solid; border-style: solid;
border-color: #555; border-color: var(--border);
} }
/* add-remove */ /* add-remove */
@ -33,6 +46,15 @@ body {
font-size: large; 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 */
#outline { #outline {
@ -51,81 +73,103 @@ summary {
} }
summary.selected { summary.selected {
color: #fff; color: var(--text-bright);
background-color: #444; background-color: var(--selection-highlight);
} }
summary > div, .cst { summary > div, .constraint {
padding-top: 4px; padding-top: 4px;
padding-bottom: 4px; padding-bottom: 4px;
} }
.elt, .cst { .element, .constraint {
display: flex; display: flex;
flex-grow: 1; flex-grow: 1;
padding-left: 8px; padding-left: 8px;
padding-right: 8px; padding-right: 8px;
} }
.elt-switch { .element-switch {
width: 18px; width: 18px;
padding-left: 2px; padding-left: 2px;
text-align: center; text-align: center;
} }
details:has(li) .elt-switch::after { details:has(li) .element-switch::after {
content: '▸'; content: '▸';
} }
details[open]:has(li) .elt-switch::after { details[open]:has(li) .element-switch::after {
content: '▾'; content: '▾';
} }
.elt-label { .element-label {
flex-grow: 1; flex-grow: 1;
} }
.cst-label { .constraint-label {
flex-grow: 1; flex-grow: 1;
} }
.elt-rep { .element-representation {
glen marked this conversation as resolved Outdated
Outdated
Review

CSS class names, like variable names, should ideally be self-documenting. The class .cst doesn't say anything to me. Ideally, choose a different class name with clearer semantics. If that's not feasible for some reason, make sure to clearly comment the first use of the .cst class.

CSS class names, like variable names, should ideally be self-documenting. The class `.cst` doesn't say anything to me. Ideally, choose a different class name with clearer semantics. If that's not feasible for some reason, make sure to clearly comment the first use of the `.cst` class.

Done! I've expanded "cst" to "constraint," "elt" to "element," and "rep" to "representation" in the HTML element classes (commits 882286c and 3f3c173).

Done! I've expanded "cst" to "constraint," "elt" to "element," and "rep" to "representation" in the HTML element classes (commits 882286c and 3f3c173).
Outdated
Review

Yes, much more intelligible! Thanks, resolving.

Yes, much more intelligible! Thanks, resolving.
display: flex; display: flex;
glen marked this conversation as resolved Outdated
Outdated
Review

I don't really mind #222 and #f0f0f0 sort of thing for various greys, because they are pretty easy to understand, but my mental hex-to-hue converter is otherwise not particularly good. Colors also often carry semantic content and/or play specific roles. So please don't use non-grey bare hex colors in css. Give them symbolic names that convey role (presuming there is one) and/or hue as well if feasible (or if not, comment the hue where the symbol is defined) and then use them symbolically, presumably with var(...) CSS references. For example, in Numberscope several colors are defined in App.vue and then used throughout the components with var(...) references. Thanks.

I don't really mind `#222` and `#f0f0f0` sort of thing for various greys, because they are pretty easy to understand, but my mental hex-to-hue converter is otherwise not particularly good. Colors also often carry semantic content and/or play specific roles. So please don't use non-grey bare hex colors in css. Give them symbolic names that convey role (presuming there is one) and/or hue as well if feasible (or if not, comment the hue where the symbol is defined) and then use them symbolically, presumably with `var(...)` CSS references. For example, in Numberscope several colors are defined in App.vue and then used throughout the components with `var(...)` references. Thanks.

Done (in commit 0c8022d). I've named the color variables according to their roles, with cues about the intended color limited to brightness descriptions. There are comments describing the colors on the lines where the color variables are defined. I'm hoping that this will make it easier to keep the color descriptions up to date with the actual colors.

Done (in commit 0c8022d). I've named the color variables according to their roles, with cues about the intended color limited to brightness descriptions. There are comments describing the colors on the lines where the color variables are defined. I'm hoping that this will make it easier to keep the color descriptions up to date with the actual colors.
Outdated
Review

Those are excellent symbolic names and comments. If we are ever pulling in an external package that has css variables of its own, we will need to add a prefix (e.g., --text-bright becomes --dyna-text-bright), but they're good for now.

Those are excellent symbolic names and comments. If we are ever pulling in an external package that has css variables of its own, we will need to add a prefix (e.g., --text-bright becomes --dyna-text-bright), but they're good for now.
} }
.elt-rep > div { .element-representation > div {
padding: 2px 0px 0px 0px; padding: 2px 0px 0px 0px;
font-size: 10pt; font-size: 10pt;
text-align: center; font-variant-numeric: tabular-nums;
text-align: right;
width: 56px; width: 56px;
} }
.cst { .constraint {
font-style: italic; font-style: italic;
} }
.cst > input[type=checkbox] { .constraint.invalid {
color: var(--text-invalid);
}
.constraint > input[type=checkbox] {
margin: 0px 8px 0px 0px; margin: 0px 8px 0px 0px;
} }
.cst > input[type=text] { .constraint > input[type=text] {
color: #fcfcfc; color: inherit;
background-color: inherit; background-color: inherit;
border: 1px solid #555; border: 1px solid var(--border);
border-radius: 2px; 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 */ /* display */
canvas { canvas {
float: left; float: left;
margin-left: 20px; margin-left: 20px;
margin-top: 20px; margin-top: 20px;
background-color: #020202; background-color: var(--display-background);
border: 1px solid #555; border: 1px solid var(--border);
border-radius: 16px; border-radius: 16px;
} }
canvas:focus { 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 sycamore::prelude::*;
use web_sys::{console, wasm_bindgen::JsValue}; use web_sys::{console, wasm_bindgen::JsValue};
use crate::{engine, AppState, assembly::{Assembly, Constraint, Element}}; use crate::{engine, AppState, assembly::{Assembly, Constraint, Element}};
/* DEBUG */ /* DEBUG */
// load an example assembly for testing. this code will be removed once we've
glen marked this conversation as resolved Outdated
Outdated
Review

Shouldn't these debug functions be in some kind of a test and/or conditionally compiled only if one is testing? They look a bit, um, specific for functions that would actually be compiled into a webpage...

Shouldn't these debug functions be in some kind of a test and/or conditionally compiled only if one is testing? They look a bit, um, specific for functions that would actually be compiled into a webpage...

These functions build the test assemblies we've been playing with: the "General" assembly that appears when you load the page, and the "Low-curvature" assembly that you can pick from the drop-down menu. They're always compiled because I've never wanted to build the app with the test configurations removed.

It would useful to build a more formal test assembly system at some point. That task seems pretty self-contained, so I think it should be its own PR. I also think it'll be easier once we have a system for saving and loading assemblies.

These functions build the test assemblies we've been playing with: the "General" assembly that appears when you load the page, and the "Low-curvature" assembly that you can pick from the drop-down menu. They're always compiled because I've never wanted to build the app with the test configurations removed. It would useful to build a more formal test assembly system at some point. That task seems pretty self-contained, so I think it should be its own PR. I also think it'll be easier once we have a system for saving and loading assemblies.
Outdated
Review

Ah, so they are not debug functions; in fact they are the current "Assembly gallery" or "Assembly examples". So just rename/re-comment things to clarify their current role, and add a way to easily/quickly get to the empty assembly as well (or put an issue to do that in a future PR), and all should be well.

Ah, so they are not debug functions; in fact they are the current "Assembly gallery" or "Assembly examples". So just rename/re-comment things to clarify their current role, and add a way to easily/quickly get to the empty assembly as well (or put an issue to do that in a future PR), and all should be well.

Done (in commit a48fef3).

Done (in commit a48fef3).
Outdated
Review

It's also quite plausible to me that some examples will move into the dyna3 equivalent of a "featured gallery", but that idea may not deserve to be in a comment at this point.

It's also quite plausible to me that some examples will move into the dyna3 equivalent of a "featured gallery", but that idea may not deserve to be in a comment at this point.
// built a more formal test assembly system
fn load_gen_assemb(assembly: &Assembly) { fn load_gen_assemb(assembly: &Assembly) {
let _ = assembly.try_insert_element( let _ = assembly.try_insert_element(
Element { Element::new(
id: String::from("gemini_a"), String::from("gemini_a"),
label: String::from("Castor"), String::from("Castor"),
color: [1.00_f32, 0.25_f32, 0.00_f32], [1.00_f32, 0.25_f32, 0.00_f32],
representation: engine::sphere(0.5, 0.5, 0.0, 1.0), engine::sphere(0.5, 0.5, 0.0, 1.0)
constraints: BTreeSet::default(), )
index: 0
}
); );
let _ = assembly.try_insert_element( let _ = assembly.try_insert_element(
Element { Element::new(
id: String::from("gemini_b"), String::from("gemini_b"),
label: String::from("Pollux"), String::from("Pollux"),
color: [0.00_f32, 0.25_f32, 1.00_f32], [0.00_f32, 0.25_f32, 1.00_f32],
representation: engine::sphere(-0.5, -0.5, 0.0, 1.0), engine::sphere(-0.5, -0.5, 0.0, 1.0)
constraints: BTreeSet::default(), )
index: 0
}
); );
let _ = assembly.try_insert_element( let _ = assembly.try_insert_element(
Element { Element::new(
id: String::from("ursa_major"), String::from("ursa_major"),
label: String::from("Ursa major"), String::from("Ursa major"),
color: [0.25_f32, 0.00_f32, 1.00_f32], [0.25_f32, 0.00_f32, 1.00_f32],
representation: engine::sphere(-0.5, 0.5, 0.0, 0.75), engine::sphere(-0.5, 0.5, 0.0, 0.75)
constraints: BTreeSet::default(), )
index: 0
}
); );
let _ = assembly.try_insert_element( let _ = assembly.try_insert_element(
Element { Element::new(
id: String::from("ursa_minor"), String::from("ursa_minor"),
label: String::from("Ursa minor"), String::from("Ursa minor"),
color: [0.25_f32, 1.00_f32, 0.00_f32], [0.25_f32, 1.00_f32, 0.00_f32],
representation: engine::sphere(0.5, -0.5, 0.0, 0.5), engine::sphere(0.5, -0.5, 0.0, 0.5)
constraints: BTreeSet::default(), )
index: 0
}
); );
let _ = assembly.try_insert_element( let _ = assembly.try_insert_element(
Element { Element::new(
id: String::from("moon_deimos"), String::from("moon_deimos"),
label: String::from("Deimos"), String::from("Deimos"),
color: [0.75_f32, 0.75_f32, 0.00_f32], [0.75_f32, 0.75_f32, 0.00_f32],
representation: engine::sphere(0.0, 0.15, 1.0, 0.25), engine::sphere(0.0, 0.15, 1.0, 0.25)
constraints: BTreeSet::default(), )
index: 0
}
); );
let _ = assembly.try_insert_element( let _ = assembly.try_insert_element(
Element { Element::new(
id: String::from("moon_phobos"), String::from("moon_phobos"),
label: String::from("Phobos"), String::from("Phobos"),
color: [0.00_f32, 0.75_f32, 0.50_f32], [0.00_f32, 0.75_f32, 0.50_f32],
representation: engine::sphere(0.0, -0.15, -1.0, 0.25), engine::sphere(0.0, -0.15, -1.0, 0.25)
constraints: BTreeSet::default(), )
index: 0
}
); );
} }
/* DEBUG */ /* 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) { fn load_low_curv_assemb(assembly: &Assembly) {
let a = 0.75_f64.sqrt(); let a = 0.75_f64.sqrt();
let _ = assembly.try_insert_element( let _ = assembly.try_insert_element(
Element { Element::new(
id: "central".to_string(), "central".to_string(),
label: "Central".to_string(), "Central".to_string(),
color: [0.75_f32, 0.75_f32, 0.75_f32], [0.75_f32, 0.75_f32, 0.75_f32],
representation: engine::sphere(0.0, 0.0, 0.0, 1.0), engine::sphere(0.0, 0.0, 0.0, 1.0)
constraints: BTreeSet::default(), )
index: 0
}
); );
let _ = assembly.try_insert_element( let _ = assembly.try_insert_element(
Element { Element::new(
id: "assemb_plane".to_string(), "assemb_plane".to_string(),
label: "Assembly plane".to_string(), "Assembly plane".to_string(),
color: [0.75_f32, 0.75_f32, 0.75_f32], [0.75_f32, 0.75_f32, 0.75_f32],
representation: engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0), engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0)
constraints: BTreeSet::default(), )
index: 0
}
); );
let _ = assembly.try_insert_element( let _ = assembly.try_insert_element(
Element { Element::new(
id: "side1".to_string(), "side1".to_string(),
label: "Side 1".to_string(), "Side 1".to_string(),
color: [1.00_f32, 0.00_f32, 0.25_f32], [1.00_f32, 0.00_f32, 0.25_f32],
representation: engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0), engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0)
constraints: BTreeSet::default(), )
index: 0
}
); );
let _ = assembly.try_insert_element( let _ = assembly.try_insert_element(
Element { Element::new(
id: "side2".to_string(), "side2".to_string(),
label: "Side 2".to_string(), "Side 2".to_string(),
color: [0.25_f32, 1.00_f32, 0.00_f32], [0.25_f32, 1.00_f32, 0.00_f32],
representation: engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0), engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0)
constraints: BTreeSet::default(), )
index: 0
}
); );
let _ = assembly.try_insert_element( let _ = assembly.try_insert_element(
Element { Element::new(
id: "side3".to_string(), "side3".to_string(),
label: "Side 3".to_string(), "Side 3".to_string(),
color: [0.00_f32, 0.25_f32, 1.00_f32], [0.00_f32, 0.25_f32, 1.00_f32],
representation: engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0), engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0)
constraints: BTreeSet::default(), )
index: 0
}
); );
let _ = assembly.try_insert_element( let _ = assembly.try_insert_element(
Element { Element::new(
id: "corner1".to_string(), "corner1".to_string(),
label: "Corner 1".to_string(), "Corner 1".to_string(),
color: [0.75_f32, 0.75_f32, 0.75_f32], [0.75_f32, 0.75_f32, 0.75_f32],
representation: engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0), engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0)
constraints: BTreeSet::default(), )
index: 0
}
); );
let _ = assembly.try_insert_element( let _ = assembly.try_insert_element(
Element { Element::new(
id: "corner2".to_string(), "corner2".to_string(),
label: "Corner 2".to_string(), "Corner 2".to_string(),
color: [0.75_f32, 0.75_f32, 0.75_f32], [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), 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( let _ = assembly.try_insert_element(
Element { Element::new(
id: String::from("corner3"), String::from("corner3"),
label: String::from("Corner 3"), String::from("Corner 3"),
color: [0.75_f32, 0.75_f32, 0.75_f32], [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), engine::sphere(2.0/3.0, 4.0/3.0 * a, 0.0, 1.0/3.0)
constraints: BTreeSet::default(), )
index: 0
}
); );
} }
@ -202,6 +177,7 @@ pub fn AddRemove() -> View {
} }
glen marked this conversation as resolved
Review

Please elaborate all instances of KLUDGE

Please elaborate all instances of KLUDGE
Review

Done (in commit 2b083be).

Done (in commit 2b083be).
) { "+" } ) { "+" }
button( button(
class="emoji", /* KLUDGE */ // for convenience, we're using an emoji as a temporary icon for this button
disabled={ disabled={
let state = use_context::<AppState>(); let state = use_context::<AppState>();
state.selection.with(|sel| sel.len() != 2) 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 = create_signal(0.0);
let lorentz_prod_valid = create_signal(false);
let active = create_signal(true); let active = create_signal(true);
state.assembly.insert_constraint(Constraint { state.assembly.insert_constraint(Constraint {
subjects: subjects, subjects: subjects,
lorentz_prod: lorentz_prod, lorentz_prod: lorentz_prod,
lorentz_prod_text: create_signal(String::new()), lorentz_prod_text: create_signal(String::new()),
lorentz_prod_valid: create_signal(false), lorentz_prod_valid: lorentz_prod_valid,
active: active, active: active,
}); });
state.assembly.realize();
state.selection.update(|sel| sel.clear()); state.selection.update(|sel| sel.clear());
/* DEBUG */ /* DEBUG */
@ -241,23 +217,23 @@ pub fn AddRemove() -> View {
} }
}); });
// update the realization when the constraint activated, or // update the realization when the constraint becomes active
// edited while active // and valid, or is edited while active and valid
create_effect(move || { create_effect(move || {
console::log_1(&JsValue::from(
format!("Constraint ({}, {}) updated", subjects.0, subjects.1)
));
lorentz_prod.track(); lorentz_prod.track();
console::log_2( if active.get() && lorentz_prod_valid.get() {
&JsValue::from("Lorentz product updated to"),
&JsValue::from(lorentz_prod.get_untracked())
);
if active.get() {
state.assembly.realize(); state.assembly.realize();
} }
}); });
} }
) { "🔗" } ) { "🔗" }
select(bind:value=assembly_name) { /* DEBUG */ select(bind:value=assembly_name) { /* DEBUG */ // example assembly chooser
option(value="general") { "General" } option(value="general") { "General" }
option(value="low-curv") { "Low-curvature" } option(value="low-curv") { "Low-curvature" }
option(value="empty") { "Empty" }
} }
} }
} }

View File

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

View File

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

View File

@ -1,7 +1,6 @@
use itertools::Itertools; use itertools::Itertools;
use sycamore::{prelude::*, web::tags::div}; use sycamore::prelude::*;
use web_sys::{ use web_sys::{
Element,
Event, Event,
HtmlInputElement, HtmlInputElement,
KeyboardEvent, KeyboardEvent,
@ -9,7 +8,7 @@ use web_sys::{
wasm_bindgen::JsCast 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 // an editable view of the Lorentz product representing a constraint
#[component(inline_props)] #[component(inline_props)]
@ -32,6 +31,143 @@ 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() {
glen marked this conversation as resolved
Review

What's the "native" form of event.key()? Seems a pity to convert it to a str just to match it against three options. Can't constants of the actual values of the three options (before str conversion), perhaps defined above if need be, be used in place?

What's the "native" form of event.key()? Seems a pity to convert it to a str just to match it against three options. Can't constants of the actual values of the three options (before str conversion), perhaps defined above if need be, be used in place?
Review

The key method returns a String (a heap-allocated, variable-length string object). Each of the string literals we're matching with is a &str (a reference to a "string slice," which seems to be a pointer to a fixed-length string of bytes). An explicit conversion is required, and as_str seems to be the standard way to do it.

The [`key`](https://docs.rs/web-sys/latest/web_sys/struct.KeyboardEvent.html#method.key) method returns a [`String`](https://doc.rust-lang.org/nightly/alloc/string/struct.String.html) (a heap-allocated, variable-length string object). Each of the string literals we're matching with is a [`&str`](https://doc.rust-lang.org/nightly/core/primitive.str.html) (a reference to a "string slice," which seems to be a pointer to a fixed-length string of bytes). An explicit conversion is required, and [`as_str`](https://doc.rust-lang.org/nightly/alloc/string/struct.String.html#method.as_str) seems to be the [standard way](https://stackoverflow.com/questions/25383488/how-to-match-a-string-against-string-literals?rq=3) to do it.
Review

Oh, gotcha. A Rustism I was not yet used to. Hope you can see why it looked odd.

Oh, gotcha. A Rustism I was not yet used to. Hope you can see why it looked odd.
Review

Yup. Stuff like "hello".to_string() looked even weirder to me at first, but the Rust book authors seem to consider it the most idiomatic way of going in the other direction...

Yup. Stuff like `"hello".to_string()` looked even weirder to me at first, but the Rust book authors seem to consider it the most idiomatic way of going in the other direction...
Review

Well yes in fact that looks so weird that I believe that in husht we will implement a labeled-literal syntax like s"hello" for exactly this. Note that room has been left in the Rust syntax for such a thing, it's just not part of the language (yet, anyway).

Well yes in fact that looks so weird that I believe that in husht we will implement a labeled-literal syntax like `s"hello"` for exactly this. Note that room has been left in the Rust syntax for such a thing, it's just not part of the language (yet, anyway).
"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 // a component that lists the elements of the current assembly, showing the
// constraints on each element as a collapsible sub-list. its implementation // constraints on each element as a collapsible sub-list. its implementation
// is based on Kate Morley's HTML + CSS tree views: // is based on Kate Morley's HTML + CSS tree views:
@ -40,15 +176,16 @@ fn LorentzProductInput(constraint: Constraint) -> View {
// //
#[component] #[component]
pub fn Outline() -> View { pub fn Outline() -> View {
// sort the elements alphabetically by ID let state = use_context::<AppState>();
let elements_sorted = create_memo(|| {
let state = use_context::<AppState>(); // list the elements alphabetically by ID
state.assembly.elements let element_list = state.assembly.elements.map(
.get_clone() |elts| elts
.clone()
.into_iter() .into_iter()
.sorted_by_key(|(_, elt)| elt.id.clone()) .sorted_by_key(|(_, elt)| elt.id.clone())
.collect() .collect()
}); );
view! { view! {
ul( ul(
@ -59,128 +196,11 @@ pub fn Outline() -> View {
} }
) { ) {
Keyed( Keyed(
list=elements_sorted, list=element_list,
view=|(key, elt)| { view=|(key, elt)| view! {
let state = use_context::<AppState>(); ElementOutlineItem(key=key, element=elt)
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, elt)| ( key=|(key, _)| key.clone()
key.clone(),
elt.id.clone(),
elt.label.clone(),
elt.constraints.clone()
)
) )
} }
} }