forked from StudioInfinity/dyna3
feat: Points (#82)
Replaces the former sole Element entity by two, Sphere and Point, both implementing an Element trait. Adds Point display, uses the former Element display for Sphere. Adds a new "canned" configuration, and the ability to add, select, and nudge Point entities. Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo> Reviewed-on: StudioInfinity/dyna3#82 Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net> Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
This commit is contained in:
parent
360ce12d8b
commit
a2478febc1
9 changed files with 815 additions and 330 deletions
|
@ -42,9 +42,7 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
#add-remove > button {
|
#add-remove > button {
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
height: 32px;
|
||||||
font-size: large;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* KLUDGE */
|
/* KLUDGE */
|
||||||
|
@ -53,7 +51,9 @@ body {
|
||||||
buttons need to be displayed in an emoji font
|
buttons need to be displayed in an emoji font
|
||||||
*/
|
*/
|
||||||
#add-remove > button.emoji {
|
#add-remove > button.emoji {
|
||||||
|
width: 32px;
|
||||||
font-family: 'Noto Emoji', sans-serif;
|
font-family: 'Noto Emoji', sans-serif;
|
||||||
|
font-size: large;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* outline */
|
/* outline */
|
||||||
|
|
|
@ -1,58 +1,59 @@
|
||||||
|
use std::{f64::consts::FRAC_1_SQRT_2, rc::Rc};
|
||||||
use sycamore::prelude::*;
|
use sycamore::prelude::*;
|
||||||
use web_sys::{console, wasm_bindgen::JsValue};
|
use web_sys::{console, wasm_bindgen::JsValue};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
engine,
|
engine,
|
||||||
AppState,
|
AppState,
|
||||||
assembly::{Assembly, Element, InversiveDistanceRegulator}
|
assembly::{Assembly, InversiveDistanceRegulator, Point, Sphere}
|
||||||
};
|
};
|
||||||
|
|
||||||
/* DEBUG */
|
/* DEBUG */
|
||||||
// load an example assembly for testing. this code will be removed once we've
|
// load an example assembly for testing. this code will be removed once we've
|
||||||
// built a more formal test assembly system
|
// built a more formal test assembly system
|
||||||
fn load_gen_assemb(assembly: &Assembly) {
|
fn load_gen_assemb(assembly: &Assembly) {
|
||||||
let _ = assembly.try_insert_sphere(
|
let _ = assembly.try_insert_element(
|
||||||
Element::new(
|
Sphere::new(
|
||||||
String::from("gemini_a"),
|
String::from("gemini_a"),
|
||||||
String::from("Castor"),
|
String::from("Castor"),
|
||||||
[1.00_f32, 0.25_f32, 0.00_f32],
|
[1.00_f32, 0.25_f32, 0.00_f32],
|
||||||
engine::sphere(0.5, 0.5, 0.0, 1.0)
|
engine::sphere(0.5, 0.5, 0.0, 1.0)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
let _ = assembly.try_insert_sphere(
|
let _ = assembly.try_insert_element(
|
||||||
Element::new(
|
Sphere::new(
|
||||||
String::from("gemini_b"),
|
String::from("gemini_b"),
|
||||||
String::from("Pollux"),
|
String::from("Pollux"),
|
||||||
[0.00_f32, 0.25_f32, 1.00_f32],
|
[0.00_f32, 0.25_f32, 1.00_f32],
|
||||||
engine::sphere(-0.5, -0.5, 0.0, 1.0)
|
engine::sphere(-0.5, -0.5, 0.0, 1.0)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
let _ = assembly.try_insert_sphere(
|
let _ = assembly.try_insert_element(
|
||||||
Element::new(
|
Sphere::new(
|
||||||
String::from("ursa_major"),
|
String::from("ursa_major"),
|
||||||
String::from("Ursa major"),
|
String::from("Ursa major"),
|
||||||
[0.25_f32, 0.00_f32, 1.00_f32],
|
[0.25_f32, 0.00_f32, 1.00_f32],
|
||||||
engine::sphere(-0.5, 0.5, 0.0, 0.75)
|
engine::sphere(-0.5, 0.5, 0.0, 0.75)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
let _ = assembly.try_insert_sphere(
|
let _ = assembly.try_insert_element(
|
||||||
Element::new(
|
Sphere::new(
|
||||||
String::from("ursa_minor"),
|
String::from("ursa_minor"),
|
||||||
String::from("Ursa minor"),
|
String::from("Ursa minor"),
|
||||||
[0.25_f32, 1.00_f32, 0.00_f32],
|
[0.25_f32, 1.00_f32, 0.00_f32],
|
||||||
engine::sphere(0.5, -0.5, 0.0, 0.5)
|
engine::sphere(0.5, -0.5, 0.0, 0.5)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
let _ = assembly.try_insert_sphere(
|
let _ = assembly.try_insert_element(
|
||||||
Element::new(
|
Sphere::new(
|
||||||
String::from("moon_deimos"),
|
String::from("moon_deimos"),
|
||||||
String::from("Deimos"),
|
String::from("Deimos"),
|
||||||
[0.75_f32, 0.75_f32, 0.00_f32],
|
[0.75_f32, 0.75_f32, 0.00_f32],
|
||||||
engine::sphere(0.0, 0.15, 1.0, 0.25)
|
engine::sphere(0.0, 0.15, 1.0, 0.25)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
let _ = assembly.try_insert_sphere(
|
let _ = assembly.try_insert_element(
|
||||||
Element::new(
|
Sphere::new(
|
||||||
String::from("moon_phobos"),
|
String::from("moon_phobos"),
|
||||||
String::from("Phobos"),
|
String::from("Phobos"),
|
||||||
[0.00_f32, 0.75_f32, 0.50_f32],
|
[0.00_f32, 0.75_f32, 0.50_f32],
|
||||||
|
@ -66,64 +67,64 @@ fn load_gen_assemb(assembly: &Assembly) {
|
||||||
// built a more formal test assembly system
|
// 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_sphere(
|
let _ = assembly.try_insert_element(
|
||||||
Element::new(
|
Sphere::new(
|
||||||
"central".to_string(),
|
"central".to_string(),
|
||||||
"Central".to_string(),
|
"Central".to_string(),
|
||||||
[0.75_f32, 0.75_f32, 0.75_f32],
|
[0.75_f32, 0.75_f32, 0.75_f32],
|
||||||
engine::sphere(0.0, 0.0, 0.0, 1.0)
|
engine::sphere(0.0, 0.0, 0.0, 1.0)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
let _ = assembly.try_insert_sphere(
|
let _ = assembly.try_insert_element(
|
||||||
Element::new(
|
Sphere::new(
|
||||||
"assemb_plane".to_string(),
|
"assemb_plane".to_string(),
|
||||||
"Assembly plane".to_string(),
|
"Assembly plane".to_string(),
|
||||||
[0.75_f32, 0.75_f32, 0.75_f32],
|
[0.75_f32, 0.75_f32, 0.75_f32],
|
||||||
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)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
let _ = assembly.try_insert_sphere(
|
let _ = assembly.try_insert_element(
|
||||||
Element::new(
|
Sphere::new(
|
||||||
"side1".to_string(),
|
"side1".to_string(),
|
||||||
"Side 1".to_string(),
|
"Side 1".to_string(),
|
||||||
[1.00_f32, 0.00_f32, 0.25_f32],
|
[1.00_f32, 0.00_f32, 0.25_f32],
|
||||||
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)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
let _ = assembly.try_insert_sphere(
|
let _ = assembly.try_insert_element(
|
||||||
Element::new(
|
Sphere::new(
|
||||||
"side2".to_string(),
|
"side2".to_string(),
|
||||||
"Side 2".to_string(),
|
"Side 2".to_string(),
|
||||||
[0.25_f32, 1.00_f32, 0.00_f32],
|
[0.25_f32, 1.00_f32, 0.00_f32],
|
||||||
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)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
let _ = assembly.try_insert_sphere(
|
let _ = assembly.try_insert_element(
|
||||||
Element::new(
|
Sphere::new(
|
||||||
"side3".to_string(),
|
"side3".to_string(),
|
||||||
"Side 3".to_string(),
|
"Side 3".to_string(),
|
||||||
[0.00_f32, 0.25_f32, 1.00_f32],
|
[0.00_f32, 0.25_f32, 1.00_f32],
|
||||||
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)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
let _ = assembly.try_insert_sphere(
|
let _ = assembly.try_insert_element(
|
||||||
Element::new(
|
Sphere::new(
|
||||||
"corner1".to_string(),
|
"corner1".to_string(),
|
||||||
"Corner 1".to_string(),
|
"Corner 1".to_string(),
|
||||||
[0.75_f32, 0.75_f32, 0.75_f32],
|
[0.75_f32, 0.75_f32, 0.75_f32],
|
||||||
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)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
let _ = assembly.try_insert_sphere(
|
let _ = assembly.try_insert_element(
|
||||||
Element::new(
|
Sphere::new(
|
||||||
"corner2".to_string(),
|
"corner2".to_string(),
|
||||||
"Corner 2".to_string(),
|
"Corner 2".to_string(),
|
||||||
[0.75_f32, 0.75_f32, 0.75_f32],
|
[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)
|
engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
let _ = assembly.try_insert_sphere(
|
let _ = assembly.try_insert_element(
|
||||||
Element::new(
|
Sphere::new(
|
||||||
String::from("corner3"),
|
String::from("corner3"),
|
||||||
String::from("Corner 3"),
|
String::from("Corner 3"),
|
||||||
[0.75_f32, 0.75_f32, 0.75_f32],
|
[0.75_f32, 0.75_f32, 0.75_f32],
|
||||||
|
@ -132,6 +133,49 @@ fn load_low_curv_assemb(assembly: &Assembly) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn load_pointed_assemb(assembly: &Assembly) {
|
||||||
|
let _ = assembly.try_insert_element(
|
||||||
|
Point::new(
|
||||||
|
format!("point_front"),
|
||||||
|
format!("Front point"),
|
||||||
|
[0.75_f32, 0.75_f32, 0.75_f32],
|
||||||
|
engine::point(0.0, 0.0, FRAC_1_SQRT_2)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
let _ = assembly.try_insert_element(
|
||||||
|
Point::new(
|
||||||
|
format!("point_back"),
|
||||||
|
format!("Back point"),
|
||||||
|
[0.75_f32, 0.75_f32, 0.75_f32],
|
||||||
|
engine::point(0.0, 0.0, -FRAC_1_SQRT_2)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
for index_x in 0..=1 {
|
||||||
|
for index_y in 0..=1 {
|
||||||
|
let x = index_x as f64 - 0.5;
|
||||||
|
let y = index_y as f64 - 0.5;
|
||||||
|
|
||||||
|
let _ = assembly.try_insert_element(
|
||||||
|
Sphere::new(
|
||||||
|
format!("sphere{index_x}{index_y}"),
|
||||||
|
format!("Sphere {index_x}{index_y}"),
|
||||||
|
[0.5*(1.0 + x) as f32, 0.5*(1.0 + y) as f32, 0.5*(1.0 - x*y) as f32],
|
||||||
|
engine::sphere(x, y, 0.0, 1.0)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = assembly.try_insert_element(
|
||||||
|
Point::new(
|
||||||
|
format!("point{index_x}{index_y}"),
|
||||||
|
format!("Point {index_x}{index_y}"),
|
||||||
|
[0.5*(1.0 + x) as f32, 0.5*(1.0 + y) as f32, 0.5*(1.0 - x*y) as f32],
|
||||||
|
engine::point(x, y, 0.0)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn AddRemove() -> View {
|
pub fn AddRemove() -> View {
|
||||||
/* DEBUG */
|
/* DEBUG */
|
||||||
|
@ -157,6 +201,7 @@ pub fn AddRemove() -> View {
|
||||||
match name.as_str() {
|
match name.as_str() {
|
||||||
"general" => load_gen_assemb(assembly),
|
"general" => load_gen_assemb(assembly),
|
||||||
"low-curv" => load_low_curv_assemb(assembly),
|
"low-curv" => load_low_curv_assemb(assembly),
|
||||||
|
"pointed" => load_pointed_assemb(assembly),
|
||||||
_ => ()
|
_ => ()
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -167,9 +212,15 @@ pub fn AddRemove() -> View {
|
||||||
button(
|
button(
|
||||||
on:click=|_| {
|
on:click=|_| {
|
||||||
let state = use_context::<AppState>();
|
let state = use_context::<AppState>();
|
||||||
state.assembly.insert_new_sphere();
|
state.assembly.insert_element_default::<Sphere>();
|
||||||
}
|
}
|
||||||
) { "+" }
|
) { "Add sphere" }
|
||||||
|
button(
|
||||||
|
on:click=|_| {
|
||||||
|
let state = use_context::<AppState>();
|
||||||
|
state.assembly.insert_element_default::<Point>();
|
||||||
|
}
|
||||||
|
) { "Add point" }
|
||||||
button(
|
button(
|
||||||
class="emoji", /* KLUDGE */ // for convenience, we're using an emoji as a temporary icon for this button
|
class="emoji", /* KLUDGE */ // for convenience, we're using an emoji as a temporary icon for this button
|
||||||
disabled={
|
disabled={
|
||||||
|
@ -190,7 +241,7 @@ pub fn AddRemove() -> View {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
);
|
);
|
||||||
state.assembly.insert_regulator(
|
state.assembly.insert_regulator(
|
||||||
InversiveDistanceRegulator::new(subjects, &state.assembly)
|
Rc::new(InversiveDistanceRegulator::new(subjects, &state.assembly))
|
||||||
);
|
);
|
||||||
state.selection.update(|sel| sel.clear());
|
state.selection.update(|sel| sel.clear());
|
||||||
}
|
}
|
||||||
|
@ -198,6 +249,7 @@ pub fn AddRemove() -> View {
|
||||||
select(bind:value=assembly_name) { /* DEBUG */ // example assembly chooser
|
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="pointed") { "Pointed" }
|
||||||
option(value="empty") { "Empty" }
|
option(value="empty") { "Empty" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,23 @@
|
||||||
use nalgebra::{DMatrix, DVector, DVectorView, Vector3};
|
use nalgebra::{DMatrix, DVector, DVectorView};
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
use slab::Slab;
|
use slab::Slab;
|
||||||
use std::{collections::BTreeSet, rc::Rc, sync::atomic::{AtomicU64, Ordering}};
|
use std::{
|
||||||
|
any::{Any, TypeId},
|
||||||
|
cell::Cell,
|
||||||
|
collections::BTreeSet,
|
||||||
|
rc::Rc,
|
||||||
|
sync::atomic::{AtomicU64, Ordering}
|
||||||
|
};
|
||||||
use sycamore::prelude::*;
|
use sycamore::prelude::*;
|
||||||
use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */
|
use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
display::DisplayItem,
|
||||||
engine::{
|
engine::{
|
||||||
Q,
|
Q,
|
||||||
change_half_curvature,
|
change_half_curvature,
|
||||||
local_unif_to_std,
|
local_unif_to_std,
|
||||||
|
point,
|
||||||
realize_gram,
|
realize_gram,
|
||||||
sphere,
|
sphere,
|
||||||
ConfigSubspace,
|
ConfigSubspace,
|
||||||
|
@ -33,31 +41,87 @@ pub type ElementColor = [f32; 3];
|
||||||
static NEXT_ELEMENT_SERIAL: AtomicU64 = AtomicU64::new(0);
|
static NEXT_ELEMENT_SERIAL: AtomicU64 = AtomicU64::new(0);
|
||||||
|
|
||||||
pub trait ProblemPoser {
|
pub trait ProblemPoser {
|
||||||
fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab<Element>);
|
fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab<Rc<dyn Element>>);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
pub trait Element: ProblemPoser + DisplayItem {
|
||||||
pub struct Element {
|
// 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>>;
|
||||||
|
|
||||||
|
// a serial number that uniquely identifies this element
|
||||||
|
fn serial(&self) -> u64;
|
||||||
|
|
||||||
|
// take the next serial number, panicking if that was the last one left
|
||||||
|
fn next_serial() -> u64 where Self: Sized {
|
||||||
|
// the technique we use to panic on overflow is taken from _Rust Atomics
|
||||||
|
// and Locks_, by Mara Bos
|
||||||
|
//
|
||||||
|
// https://marabos.nl/atomics/atomics.html#example-handle-overflow
|
||||||
|
//
|
||||||
|
NEXT_ELEMENT_SERIAL.fetch_update(
|
||||||
|
Ordering::SeqCst, Ordering::SeqCst,
|
||||||
|
|serial| serial.checked_add(1)
|
||||||
|
).expect("Out of serial numbers for elements")
|
||||||
|
}
|
||||||
|
|
||||||
|
// the configuration matrix column index that was assigned to the element
|
||||||
|
// last time the assembly was realized, or `None` if the element has never
|
||||||
|
// been through a realization
|
||||||
|
fn column_index(&self) -> Option<usize>;
|
||||||
|
|
||||||
|
// assign the element a configuration matrix column index. this method must
|
||||||
|
// be used carefully to preserve invariant (1), described in the comment on
|
||||||
|
// the `tangent` field of the `Assembly` structure
|
||||||
|
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 PartialEq for ElementRc {
|
||||||
|
fn eq(&self, ElementRc(other): &Self) -> bool {
|
||||||
|
let ElementRc(rc) = self;
|
||||||
|
Rc::ptr_eq(rc, &other)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Sphere {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub label: String,
|
pub label: String,
|
||||||
pub color: ElementColor,
|
pub color: ElementColor,
|
||||||
pub representation: Signal<DVector<f64>>,
|
pub representation: Signal<DVector<f64>>,
|
||||||
|
|
||||||
// the regulators this element is subject to. the assembly that owns the
|
|
||||||
// element is responsible for keeping this set up to date
|
|
||||||
pub regulators: Signal<BTreeSet<RegulatorKey>>,
|
pub regulators: Signal<BTreeSet<RegulatorKey>>,
|
||||||
|
|
||||||
// a serial number, assigned by `Element::new`, that uniquely identifies
|
|
||||||
// each element
|
|
||||||
pub serial: u64,
|
pub serial: u64,
|
||||||
|
column_index: Cell<Option<usize>>
|
||||||
// the configuration matrix column index that was assigned to this element
|
|
||||||
// last time the assembly was realized, or `None` if the element has never
|
|
||||||
// been through a realization
|
|
||||||
column_index: Option<usize>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Element {
|
impl Sphere {
|
||||||
const CURVATURE_COMPONENT: usize = 3;
|
const CURVATURE_COMPONENT: usize = 3;
|
||||||
|
|
||||||
pub fn new(
|
pub fn new(
|
||||||
|
@ -65,83 +129,161 @@ impl Element {
|
||||||
label: String,
|
label: String,
|
||||||
color: ElementColor,
|
color: ElementColor,
|
||||||
representation: DVector<f64>
|
representation: DVector<f64>
|
||||||
) -> Element {
|
) -> Sphere {
|
||||||
// take the next serial number, panicking if that was the last number we
|
Sphere {
|
||||||
// had left. the technique we use to panic on overflow is taken from
|
|
||||||
// _Rust Atomics and Locks_, by Mara Bos
|
|
||||||
//
|
|
||||||
// https://marabos.nl/atomics/atomics.html#example-handle-overflow
|
|
||||||
//
|
|
||||||
let serial = NEXT_ELEMENT_SERIAL.fetch_update(
|
|
||||||
Ordering::SeqCst, Ordering::SeqCst,
|
|
||||||
|serial| serial.checked_add(1)
|
|
||||||
).expect("Out of serial numbers for elements");
|
|
||||||
|
|
||||||
Element {
|
|
||||||
id: id,
|
id: id,
|
||||||
label: label,
|
label: label,
|
||||||
color: color,
|
color: color,
|
||||||
representation: create_signal(representation),
|
representation: create_signal(representation),
|
||||||
regulators: create_signal(BTreeSet::default()),
|
regulators: create_signal(BTreeSet::default()),
|
||||||
serial: serial,
|
serial: Self::next_serial(),
|
||||||
column_index: None
|
column_index: None.into()
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// the smallest positive depth, represented as a multiple of `dir`, where
|
|
||||||
// the line generated by `dir` hits the element (which is assumed to be a
|
|
||||||
// sphere). returns `None` if the line misses the sphere. this function
|
|
||||||
// should be kept synchronized with `sphere_cast` in `inversive.frag`, which
|
|
||||||
// does essentially the same thing on the GPU side
|
|
||||||
pub fn cast(&self, dir: Vector3<f64>, assembly_to_world: &DMatrix<f64>) -> Option<f64> {
|
|
||||||
// if `a/b` is less than this threshold, we approximate
|
|
||||||
// `a*u^2 + b*u + c` by the linear function `b*u + c`
|
|
||||||
const DEG_THRESHOLD: f64 = 1e-9;
|
|
||||||
|
|
||||||
let rep = self.representation.with_untracked(|rep| assembly_to_world * rep);
|
|
||||||
let a = -rep[3] * dir.norm_squared();
|
|
||||||
let b = rep.rows_range(..3).dot(&dir);
|
|
||||||
let c = -rep[4];
|
|
||||||
|
|
||||||
let adjust = 4.0*a*c/(b*b);
|
|
||||||
if adjust < 1.0 {
|
|
||||||
// as long as `b` is non-zero, the linear approximation of
|
|
||||||
//
|
|
||||||
// a*u^2 + b*u + c
|
|
||||||
//
|
|
||||||
// at `u = 0` will reach zero at a finite depth `u_lin`. the root of
|
|
||||||
// the quadratic adjacent to `u_lin` is stored in `lin_root`. if
|
|
||||||
// both roots have the same sign, `lin_root` will be the one closer
|
|
||||||
// to `u = 0`
|
|
||||||
let square_rect_ratio = 1.0 + (1.0 - adjust).sqrt();
|
|
||||||
let lin_root = -(2.0*c)/b / square_rect_ratio;
|
|
||||||
if a.abs() > DEG_THRESHOLD * b.abs() {
|
|
||||||
if lin_root > 0.0 {
|
|
||||||
Some(lin_root)
|
|
||||||
} else {
|
|
||||||
let other_root = -b/(2.*a) * square_rect_ratio;
|
|
||||||
(other_root > 0.0).then_some(other_root)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
(lin_root > 0.0).then_some(lin_root)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// the line through `dir` misses the sphere completely
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProblemPoser for Element {
|
impl Element for Sphere {
|
||||||
fn pose(&self, problem: &mut ConstraintProblem, _elts: &Slab<Element>) {
|
fn default_id() -> String {
|
||||||
let index = self.column_index.expect(
|
"sphere".to_string()
|
||||||
format!("Element \"{}\" should be indexed before writing problem data", self.id).as_str()
|
}
|
||||||
|
|
||||||
|
fn default(id: String, id_num: u64) -> Sphere {
|
||||||
|
Sphere::new(
|
||||||
|
id,
|
||||||
|
format!("Sphere {id_num}"),
|
||||||
|
[0.75_f32, 0.75_f32, 0.75_f32],
|
||||||
|
sphere(0.0, 0.0, 0.0, 1.0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_regulators(key: ElementKey, assembly: &Assembly) -> Vec<Rc<dyn Regulator>> {
|
||||||
|
vec![Rc::new(HalfCurvatureRegulator::new(key, assembly))]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn id(&self) -> &String {
|
||||||
|
&self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn label(&self) -> &String {
|
||||||
|
&self.label
|
||||||
|
}
|
||||||
|
|
||||||
|
fn representation(&self) -> Signal<DVector<f64>> {
|
||||||
|
self.representation
|
||||||
|
}
|
||||||
|
|
||||||
|
fn regulators(&self) -> Signal<BTreeSet<RegulatorKey>> {
|
||||||
|
self.regulators
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serial(&self) -> u64 {
|
||||||
|
self.serial
|
||||||
|
}
|
||||||
|
|
||||||
|
fn column_index(&self) -> Option<usize> {
|
||||||
|
self.column_index.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_column_index(&self, index: usize) {
|
||||||
|
self.column_index.set(Some(index));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProblemPoser for Sphere {
|
||||||
|
fn pose(&self, problem: &mut ConstraintProblem, _elts: &Slab<Rc<dyn Element>>) {
|
||||||
|
let index = self.column_index().expect(
|
||||||
|
format!("Sphere \"{}\" should be indexed before writing problem data", self.id).as_str()
|
||||||
);
|
);
|
||||||
problem.gram.push_sym(index, index, 1.0);
|
problem.gram.push_sym(index, index, 1.0);
|
||||||
problem.guess.set_column(index, &self.representation.get_clone_untracked());
|
problem.guess.set_column(index, &self.representation.get_clone_untracked());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct Point {
|
||||||
|
pub id: String,
|
||||||
|
pub label: String,
|
||||||
|
pub color: ElementColor,
|
||||||
|
pub representation: Signal<DVector<f64>>,
|
||||||
|
pub regulators: Signal<BTreeSet<RegulatorKey>>,
|
||||||
|
pub serial: u64,
|
||||||
|
column_index: Cell<Option<usize>>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Point {
|
||||||
|
const WEIGHT_COMPONENT: usize = 3;
|
||||||
|
|
||||||
|
pub fn new(
|
||||||
|
id: String,
|
||||||
|
label: String,
|
||||||
|
color: ElementColor,
|
||||||
|
representation: DVector<f64>
|
||||||
|
) -> Point {
|
||||||
|
Point {
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
color,
|
||||||
|
representation: create_signal(representation),
|
||||||
|
regulators: create_signal(BTreeSet::default()),
|
||||||
|
serial: Self::next_serial(),
|
||||||
|
column_index: None.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Element for Point {
|
||||||
|
fn default_id() -> String {
|
||||||
|
"point".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default(id: String, id_num: u64) -> Point {
|
||||||
|
Point::new(
|
||||||
|
id,
|
||||||
|
format!("Point {id_num}"),
|
||||||
|
[0.75_f32, 0.75_f32, 0.75_f32],
|
||||||
|
point(0.0, 0.0, 0.0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn id(&self) -> &String {
|
||||||
|
&self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn label(&self) -> &String {
|
||||||
|
&self.label
|
||||||
|
}
|
||||||
|
|
||||||
|
fn representation(&self) -> Signal<DVector<f64>> {
|
||||||
|
self.representation
|
||||||
|
}
|
||||||
|
|
||||||
|
fn regulators(&self) -> Signal<BTreeSet<RegulatorKey>> {
|
||||||
|
self.regulators
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serial(&self) -> u64 {
|
||||||
|
self.serial
|
||||||
|
}
|
||||||
|
|
||||||
|
fn column_index(&self) -> Option<usize> {
|
||||||
|
self.column_index.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_column_index(&self, index: usize) {
|
||||||
|
self.column_index.set(Some(index));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProblemPoser for Point {
|
||||||
|
fn pose(&self, problem: &mut ConstraintProblem, _elts: &Slab<Rc<dyn Element>>) {
|
||||||
|
let index = self.column_index().expect(
|
||||||
|
format!("Point \"{}\" should be indexed before writing problem data", self.id).as_str()
|
||||||
|
);
|
||||||
|
problem.gram.push_sym(index, index, 0.0);
|
||||||
|
problem.frozen.push(Point::WEIGHT_COMPONENT, index, 0.5);
|
||||||
|
problem.guess.set_column(index, &self.representation.get_clone_untracked());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub trait Regulator: ProblemPoser + OutlineItem {
|
pub trait Regulator: ProblemPoser + OutlineItem {
|
||||||
fn subjects(&self) -> Vec<ElementKey>;
|
fn subjects(&self) -> Vec<ElementKey>;
|
||||||
fn measurement(&self) -> ReadSignal<f64>;
|
fn measurement(&self) -> ReadSignal<f64>;
|
||||||
|
@ -168,7 +310,7 @@ impl InversiveDistanceRegulator {
|
||||||
pub fn new(subjects: [ElementKey; 2], assembly: &Assembly) -> InversiveDistanceRegulator {
|
pub fn new(subjects: [ElementKey; 2], assembly: &Assembly) -> InversiveDistanceRegulator {
|
||||||
let measurement = assembly.elements.map(
|
let measurement = assembly.elements.map(
|
||||||
move |elts| {
|
move |elts| {
|
||||||
let representations = subjects.map(|subj| elts[subj].representation);
|
let representations = subjects.map(|subj| elts[subj].representation());
|
||||||
representations[0].with(|rep_0|
|
representations[0].with(|rep_0|
|
||||||
representations[1].with(|rep_1|
|
representations[1].with(|rep_1|
|
||||||
rep_0.dot(&(&*Q * rep_1))
|
rep_0.dot(&(&*Q * rep_1))
|
||||||
|
@ -198,11 +340,11 @@ impl Regulator for InversiveDistanceRegulator {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProblemPoser for InversiveDistanceRegulator {
|
impl ProblemPoser for InversiveDistanceRegulator {
|
||||||
fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab<Element>) {
|
fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab<Rc<dyn Element>>) {
|
||||||
self.set_point.with_untracked(|set_pt| {
|
self.set_point.with_untracked(|set_pt| {
|
||||||
if let Some(val) = set_pt.value {
|
if let Some(val) = set_pt.value {
|
||||||
let [row, col] = self.subjects.map(
|
let [row, col] = self.subjects.map(
|
||||||
|subj| elts[subj].column_index.expect(
|
|subj| elts[subj].column_index().expect(
|
||||||
"Subjects should be indexed before inversive distance regulator writes problem data"
|
"Subjects should be indexed before inversive distance regulator writes problem data"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -221,8 +363,8 @@ pub struct HalfCurvatureRegulator {
|
||||||
impl HalfCurvatureRegulator {
|
impl HalfCurvatureRegulator {
|
||||||
pub fn new(subject: ElementKey, assembly: &Assembly) -> HalfCurvatureRegulator {
|
pub fn new(subject: ElementKey, assembly: &Assembly) -> HalfCurvatureRegulator {
|
||||||
let measurement = assembly.elements.map(
|
let measurement = assembly.elements.map(
|
||||||
move |elts| elts[subject].representation.with(
|
move |elts| elts[subject].representation().with(
|
||||||
|rep| rep[Element::CURVATURE_COMPONENT]
|
|rep| rep[Sphere::CURVATURE_COMPONENT]
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -249,7 +391,7 @@ impl Regulator for HalfCurvatureRegulator {
|
||||||
match self.set_point.with(|set_pt| set_pt.value) {
|
match self.set_point.with(|set_pt| set_pt.value) {
|
||||||
Some(half_curv) => {
|
Some(half_curv) => {
|
||||||
let representation = assembly.elements.with_untracked(
|
let representation = assembly.elements.with_untracked(
|
||||||
|elts| elts[self.subject].representation
|
|elts| elts[self.subject].representation()
|
||||||
);
|
);
|
||||||
representation.update(
|
representation.update(
|
||||||
|rep| change_half_curvature(rep, half_curv)
|
|rep| change_half_curvature(rep, half_curv)
|
||||||
|
@ -262,13 +404,13 @@ impl Regulator for HalfCurvatureRegulator {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProblemPoser for HalfCurvatureRegulator {
|
impl ProblemPoser for HalfCurvatureRegulator {
|
||||||
fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab<Element>) {
|
fn pose(&self, problem: &mut ConstraintProblem, elts: &Slab<Rc<dyn Element>>) {
|
||||||
self.set_point.with_untracked(|set_pt| {
|
self.set_point.with_untracked(|set_pt| {
|
||||||
if let Some(val) = set_pt.value {
|
if let Some(val) = set_pt.value {
|
||||||
let col = elts[self.subject].column_index.expect(
|
let col = elts[self.subject].column_index().expect(
|
||||||
"Subject should be indexed before half-curvature regulator writes problem data"
|
"Subject should be indexed before half-curvature regulator writes problem data"
|
||||||
);
|
);
|
||||||
problem.frozen.push(Element::CURVATURE_COMPONENT, col, val);
|
problem.frozen.push(Sphere::CURVATURE_COMPONENT, col, val);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -286,7 +428,7 @@ type AssemblyMotion<'a> = Vec<ElementMotion<'a>>;
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Assembly {
|
pub struct Assembly {
|
||||||
// elements and regulators
|
// elements and regulators
|
||||||
pub elements: Signal<Slab<Element>>,
|
pub elements: Signal<Slab<Rc<dyn Element>>>,
|
||||||
pub regulators: Signal<Slab<Rc<dyn Regulator>>>,
|
pub regulators: Signal<Slab<Rc<dyn Regulator>>>,
|
||||||
|
|
||||||
// solution variety tangent space. the basis vectors are stored in
|
// solution variety tangent space. the basis vectors are stored in
|
||||||
|
@ -317,66 +459,61 @@ impl Assembly {
|
||||||
|
|
||||||
// --- inserting elements and regulators ---
|
// --- inserting elements and regulators ---
|
||||||
|
|
||||||
// insert a sphere into the assembly without checking whether we already
|
// insert an element into the assembly without checking whether we already
|
||||||
// have an element with the same identifier. any element that does have the
|
// 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
|
// same identifier will get kicked out of the `elements_by_id` index
|
||||||
fn insert_sphere_unchecked(&self, elt: Element) -> ElementKey {
|
fn insert_element_unchecked<T: Element + 'static>(&self, elt: T) -> ElementKey {
|
||||||
// insert the sphere
|
// insert the element
|
||||||
let id = elt.id.clone();
|
let id = elt.id().clone();
|
||||||
let key = self.elements.update(|elts| elts.insert(elt));
|
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));
|
self.elements_by_id.update(|elts_by_id| elts_by_id.insert(id, key));
|
||||||
|
|
||||||
// regulate the sphere's curvature
|
// create and insert the element's default regulators
|
||||||
self.insert_regulator(HalfCurvatureRegulator::new(key, &self));
|
for reg in T::default_regulators(key, &self) {
|
||||||
|
self.insert_regulator(reg);
|
||||||
|
}
|
||||||
|
|
||||||
key
|
key
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn try_insert_sphere(&self, elt: Element) -> Option<ElementKey> {
|
pub fn try_insert_element(&self, elt: impl Element + 'static) -> Option<ElementKey> {
|
||||||
let can_insert = self.elements_by_id.with_untracked(
|
let can_insert = self.elements_by_id.with_untracked(
|
||||||
|elts_by_id| !elts_by_id.contains_key(&elt.id)
|
|elts_by_id| !elts_by_id.contains_key(elt.id())
|
||||||
);
|
);
|
||||||
if can_insert {
|
if can_insert {
|
||||||
Some(self.insert_sphere_unchecked(elt))
|
Some(self.insert_element_unchecked(elt))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_new_sphere(&self) {
|
pub fn insert_element_default<T: Element + 'static>(&self) {
|
||||||
// find the next unused identifier in the default sequence
|
// find the next unused identifier in the default sequence
|
||||||
|
let default_id = T::default_id();
|
||||||
let mut id_num = 1;
|
let mut id_num = 1;
|
||||||
let mut id = format!("sphere{}", id_num);
|
let mut id = format!("{default_id}{id_num}");
|
||||||
while self.elements_by_id.with_untracked(
|
while self.elements_by_id.with_untracked(
|
||||||
|elts_by_id| elts_by_id.contains_key(&id)
|
|elts_by_id| elts_by_id.contains_key(&id)
|
||||||
) {
|
) {
|
||||||
id_num += 1;
|
id_num += 1;
|
||||||
id = format!("sphere{}", id_num);
|
id = format!("{default_id}{id_num}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// create and insert a sphere
|
// create and insert the default example of `T`
|
||||||
let _ = self.insert_sphere_unchecked(
|
let _ = self.insert_element_unchecked(T::default(id, id_num));
|
||||||
Element::new(
|
|
||||||
id,
|
|
||||||
format!("Sphere {}", id_num),
|
|
||||||
[0.75_f32, 0.75_f32, 0.75_f32],
|
|
||||||
sphere(0.0, 0.0, 0.0, 1.0)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert_regulator<T: Regulator + 'static>(&self, regulator: T) {
|
pub fn insert_regulator(&self, regulator: Rc<dyn Regulator>) {
|
||||||
// add the regulator to the assembly's regulator list
|
// add the regulator to the assembly's regulator list
|
||||||
let regulator_rc = Rc::new(regulator);
|
|
||||||
let key = self.regulators.update(
|
let key = self.regulators.update(
|
||||||
|regs| regs.insert(regulator_rc.clone())
|
|regs| regs.insert(regulator.clone())
|
||||||
);
|
);
|
||||||
|
|
||||||
// add the regulator to each subject's regulator list
|
// add the regulator to each subject's regulator list
|
||||||
let subjects = regulator_rc.subjects();
|
let subjects = regulator.subjects();
|
||||||
let subject_regulators: Vec<_> = self.elements.with_untracked(
|
let subject_regulators: Vec<_> = self.elements.with_untracked(
|
||||||
|elts| subjects.into_iter().map(
|
|elts| subjects.into_iter().map(
|
||||||
|subj| elts[subj].regulators
|
|subj| elts[subj].regulators()
|
||||||
).collect()
|
).collect()
|
||||||
);
|
);
|
||||||
for regulators in subject_regulators {
|
for regulators in subject_regulators {
|
||||||
|
@ -390,10 +527,10 @@ impl Assembly {
|
||||||
/* DEBUG */
|
/* DEBUG */
|
||||||
// log the regulator update
|
// log the regulator update
|
||||||
console::log_1(&JsValue::from(
|
console::log_1(&JsValue::from(
|
||||||
format!("Updated regulator with subjects {:?}", regulator_rc.subjects())
|
format!("Updated regulator with subjects {:?}", regulator.subjects())
|
||||||
));
|
));
|
||||||
|
|
||||||
if regulator_rc.try_activate(&self_for_effect) {
|
if regulator.try_activate(&self_for_effect) {
|
||||||
self_for_effect.realize();
|
self_for_effect.realize();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -427,7 +564,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.column_index = Some(index);
|
elt.set_column_index(index);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -482,8 +619,8 @@ impl Assembly {
|
||||||
if success {
|
if success {
|
||||||
// read out the solution
|
// read out the solution
|
||||||
for (_, elt) in self.elements.get_clone_untracked() {
|
for (_, elt) in self.elements.get_clone_untracked() {
|
||||||
elt.representation.update(
|
elt.representation().update(
|
||||||
|rep| rep.set_column(0, &config.column(elt.column_index.unwrap()))
|
|rep| rep.set_column(0, &config.column(elt.column_index().unwrap()))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -521,8 +658,8 @@ impl Assembly {
|
||||||
let mut next_column_index = realized_dim;
|
let mut next_column_index = realized_dim;
|
||||||
for elt_motion in motion.iter() {
|
for elt_motion in motion.iter() {
|
||||||
let moving_elt = &mut elts[elt_motion.key];
|
let moving_elt = &mut elts[elt_motion.key];
|
||||||
if moving_elt.column_index.is_none() {
|
if moving_elt.column_index().is_none() {
|
||||||
moving_elt.column_index = Some(next_column_index);
|
moving_elt.set_column_index(next_column_index);
|
||||||
next_column_index += 1;
|
next_column_index += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -539,7 +676,7 @@ impl Assembly {
|
||||||
// we can unwrap the column index because we know that every moving
|
// we can unwrap the column index because we know that every moving
|
||||||
// element has one at this point
|
// element has one at this point
|
||||||
let column_index = self.elements.with_untracked(
|
let column_index = self.elements.with_untracked(
|
||||||
|elts| elts[elt_motion.key].column_index.unwrap()
|
|elts| elts[elt_motion.key].column_index().unwrap()
|
||||||
);
|
);
|
||||||
|
|
||||||
if column_index < realized_dim {
|
if column_index < realized_dim {
|
||||||
|
@ -555,7 +692,7 @@ impl Assembly {
|
||||||
let mut target_column = motion_proj.column_mut(column_index);
|
let mut target_column = motion_proj.column_mut(column_index);
|
||||||
let unif_to_std = self.elements.with_untracked(
|
let unif_to_std = self.elements.with_untracked(
|
||||||
|elts| {
|
|elts| {
|
||||||
elts[elt_motion.key].representation.with_untracked(
|
elts[elt_motion.key].representation().with_untracked(
|
||||||
|rep| local_unif_to_std(rep.as_view())
|
|rep| local_unif_to_std(rep.as_view())
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -567,26 +704,27 @@ impl Assembly {
|
||||||
// step the assembly along the deformation. this changes the elements'
|
// step the assembly along the deformation. this changes the elements'
|
||||||
// normalizations, so we restore those afterward
|
// normalizations, so we restore those afterward
|
||||||
/* KLUDGE */
|
/* KLUDGE */
|
||||||
// since our test assemblies only include spheres, we assume that every
|
// for now, we only restore the normalizations of spheres
|
||||||
// element is on the 1 mass shell
|
|
||||||
for (_, elt) in self.elements.get_clone_untracked() {
|
for (_, elt) in self.elements.get_clone_untracked() {
|
||||||
elt.representation.update_silent(|rep| {
|
elt.representation().update_silent(|rep| {
|
||||||
match elt.column_index {
|
match elt.column_index() {
|
||||||
Some(column_index) => {
|
Some(column_index) => {
|
||||||
// step the assembly along the deformation
|
// step the assembly along the deformation
|
||||||
*rep += motion_proj.column(column_index);
|
*rep += motion_proj.column(column_index);
|
||||||
|
|
||||||
// restore normalization by contracting toward the last
|
if elt.type_id() == TypeId::of::<Sphere>() {
|
||||||
// coordinate axis
|
// restore normalization by contracting toward the
|
||||||
let q_sp = rep.fixed_rows::<3>(0).norm_squared();
|
// last coordinate axis
|
||||||
let half_q_lt = -2.0 * rep[3] * rep[4];
|
let q_sp = rep.fixed_rows::<3>(0).norm_squared();
|
||||||
let half_q_lt_sq = half_q_lt * half_q_lt;
|
let half_q_lt = -2.0 * rep[3] * rep[4];
|
||||||
let scaling = half_q_lt + (q_sp + half_q_lt_sq).sqrt();
|
let half_q_lt_sq = half_q_lt * half_q_lt;
|
||||||
rep.fixed_rows_mut::<4>(0).scale_mut(1.0 / scaling);
|
let scaling = half_q_lt + (q_sp + half_q_lt_sq).sqrt();
|
||||||
|
rep.fixed_rows_mut::<4>(0).scale_mut(1.0 / scaling);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
None => {
|
None => {
|
||||||
console::log_1(&JsValue::from(
|
console::log_1(&JsValue::from(
|
||||||
format!("No velocity to unpack for fresh element \"{}\"", elt.id)
|
format!("No velocity to unpack for fresh element \"{}\"", elt.id())
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -602,20 +740,14 @@ impl Assembly {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::engine;
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[should_panic(expected = "Element \"sphere\" should be indexed before writing problem data")]
|
#[should_panic(expected = "Sphere \"sphere\" should be indexed before writing problem data")]
|
||||||
fn unindexed_element_test() {
|
fn unindexed_element_test() {
|
||||||
let _ = create_root(|| {
|
let _ = create_root(|| {
|
||||||
Element::new(
|
let elt = Sphere::default("sphere".to_string(), 0);
|
||||||
"sphere".to_string(),
|
elt.pose(&mut ConstraintProblem::new(1), &Slab::new());
|
||||||
"Sphere".to_string(),
|
|
||||||
[1.0_f32, 1.0_f32, 1.0_f32],
|
|
||||||
engine::sphere(0.0, 0.0, 0.0, 1.0)
|
|
||||||
).pose(&mut ConstraintProblem::new(1), &Slab::new());
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -623,18 +755,13 @@ mod tests {
|
||||||
#[should_panic(expected = "Subjects should be indexed before inversive distance regulator writes problem data")]
|
#[should_panic(expected = "Subjects should be indexed before inversive distance regulator writes problem data")]
|
||||||
fn unindexed_subject_test_inversive_distance() {
|
fn unindexed_subject_test_inversive_distance() {
|
||||||
let _ = create_root(|| {
|
let _ = create_root(|| {
|
||||||
let mut elts = Slab::new();
|
let mut elts = Slab::<Rc<dyn Element>>::new();
|
||||||
let subjects = [0, 1].map(|k| {
|
let subjects = [0, 1].map(|k| {
|
||||||
elts.insert(
|
elts.insert(
|
||||||
Element::new(
|
Rc::new(Sphere::default(format!("sphere{k}"), k))
|
||||||
format!("sphere{k}"),
|
|
||||||
format!("Sphere {k}"),
|
|
||||||
[1.0_f32, 1.0_f32, 1.0_f32],
|
|
||||||
engine::sphere(0.0, 0.0, 0.0, 1.0)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
elts[subjects[0]].column_index = Some(0);
|
elts[subjects[0]].set_column_index(0);
|
||||||
InversiveDistanceRegulator {
|
InversiveDistanceRegulator {
|
||||||
subjects: subjects,
|
subjects: subjects,
|
||||||
measurement: create_memo(|| 0.0),
|
measurement: create_memo(|| 0.0),
|
||||||
|
|
|
@ -4,17 +4,185 @@ use sycamore::{prelude::*, motion::create_raf};
|
||||||
use web_sys::{
|
use web_sys::{
|
||||||
console,
|
console,
|
||||||
window,
|
window,
|
||||||
Element,
|
|
||||||
KeyboardEvent,
|
KeyboardEvent,
|
||||||
MouseEvent,
|
MouseEvent,
|
||||||
WebGl2RenderingContext,
|
WebGl2RenderingContext,
|
||||||
|
WebGlBuffer,
|
||||||
WebGlProgram,
|
WebGlProgram,
|
||||||
WebGlShader,
|
WebGlShader,
|
||||||
WebGlUniformLocation,
|
WebGlUniformLocation,
|
||||||
wasm_bindgen::{JsCast, JsValue}
|
wasm_bindgen::{JsCast, JsValue}
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{AppState, assembly::{ElementKey, ElementMotion}};
|
use crate::{
|
||||||
|
AppState,
|
||||||
|
assembly::{ElementKey, ElementColor, ElementMotion, Point, Sphere}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- scene data ---
|
||||||
|
|
||||||
|
struct SceneSpheres {
|
||||||
|
representations: Vec<DVector<f64>>,
|
||||||
|
colors: Vec<ElementColor>,
|
||||||
|
highlights: Vec<f32>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SceneSpheres {
|
||||||
|
fn new() -> SceneSpheres{
|
||||||
|
SceneSpheres {
|
||||||
|
representations: Vec::new(),
|
||||||
|
colors: Vec::new(),
|
||||||
|
highlights: Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn len_i32(&self) -> i32 {
|
||||||
|
self.representations.len().try_into().expect("Number of spheres must fit in a 32-bit integer")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push(&mut self, representation: DVector<f64>, color: ElementColor, highlight: f32) {
|
||||||
|
self.representations.push(representation);
|
||||||
|
self.colors.push(color);
|
||||||
|
self.highlights.push(highlight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ScenePoints {
|
||||||
|
representations: Vec<DVector<f64>>,
|
||||||
|
colors: Vec<ElementColor>,
|
||||||
|
highlights: Vec<f32>,
|
||||||
|
selections: Vec<f32>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScenePoints {
|
||||||
|
fn new() -> ScenePoints {
|
||||||
|
ScenePoints {
|
||||||
|
representations: Vec::new(),
|
||||||
|
colors: Vec::new(),
|
||||||
|
highlights: Vec::new(),
|
||||||
|
selections: Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push(&mut self, representation: DVector<f64>, color: ElementColor, highlight: f32, selected: bool) {
|
||||||
|
self.representations.push(representation);
|
||||||
|
self.colors.push(color);
|
||||||
|
self.highlights.push(highlight);
|
||||||
|
self.selections.push(if selected { 1.0 } else { 0.0 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Scene {
|
||||||
|
spheres: SceneSpheres,
|
||||||
|
points: ScenePoints
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Scene {
|
||||||
|
fn new() -> Scene {
|
||||||
|
Scene {
|
||||||
|
spheres: SceneSpheres::new(),
|
||||||
|
points: ScenePoints::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait DisplayItem {
|
||||||
|
fn show(&self, scene: &mut Scene, selected: bool);
|
||||||
|
|
||||||
|
// the smallest positive depth, represented as a multiple of `dir`, where
|
||||||
|
// the line generated by `dir` hits the element. returns `None` if the line
|
||||||
|
// misses the element
|
||||||
|
fn cast(&self, dir: Vector3<f64>, assembly_to_world: &DMatrix<f64>, pixel_size: f64) -> Option<f64>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DisplayItem for Sphere {
|
||||||
|
fn show(&self, scene: &mut Scene, selected: bool) {
|
||||||
|
const HIGHLIGHT: f32 = 0.2; /* SCAFFOLDING */
|
||||||
|
let representation = self.representation.get_clone_untracked();
|
||||||
|
let color = if selected { self.color.map(|channel| 0.2 + 0.8*channel) } else { self.color };
|
||||||
|
let highlight = if selected { 1.0 } else { HIGHLIGHT };
|
||||||
|
scene.spheres.push(representation, color, highlight);
|
||||||
|
}
|
||||||
|
|
||||||
|
// this method should be kept synchronized with `sphere_cast` in
|
||||||
|
// `spheres.frag`, which does essentially the same thing on the GPU side
|
||||||
|
fn cast(&self, dir: Vector3<f64>, assembly_to_world: &DMatrix<f64>, _pixel_size: f64) -> Option<f64> {
|
||||||
|
// if `a/b` is less than this threshold, we approximate
|
||||||
|
// `a*u^2 + b*u + c` by the linear function `b*u + c`
|
||||||
|
const DEG_THRESHOLD: f64 = 1e-9;
|
||||||
|
|
||||||
|
let rep = self.representation.with_untracked(|rep| assembly_to_world * rep);
|
||||||
|
let a = -rep[3] * dir.norm_squared();
|
||||||
|
let b = rep.rows_range(..3).dot(&dir);
|
||||||
|
let c = -rep[4];
|
||||||
|
|
||||||
|
let adjust = 4.0*a*c/(b*b);
|
||||||
|
if adjust < 1.0 {
|
||||||
|
// as long as `b` is non-zero, the linear approximation of
|
||||||
|
//
|
||||||
|
// a*u^2 + b*u + c
|
||||||
|
//
|
||||||
|
// at `u = 0` will reach zero at a finite depth `u_lin`. the root of
|
||||||
|
// the quadratic adjacent to `u_lin` is stored in `lin_root`. if
|
||||||
|
// both roots have the same sign, `lin_root` will be the one closer
|
||||||
|
// to `u = 0`
|
||||||
|
let square_rect_ratio = 1.0 + (1.0 - adjust).sqrt();
|
||||||
|
let lin_root = -(2.0*c)/b / square_rect_ratio;
|
||||||
|
if a.abs() > DEG_THRESHOLD * b.abs() {
|
||||||
|
if lin_root > 0.0 {
|
||||||
|
Some(lin_root)
|
||||||
|
} else {
|
||||||
|
let other_root = -b/(2.*a) * square_rect_ratio;
|
||||||
|
(other_root > 0.0).then_some(other_root)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(lin_root > 0.0).then_some(lin_root)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// the line through `dir` misses the sphere completely
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DisplayItem for Point {
|
||||||
|
fn show(&self, scene: &mut Scene, selected: bool) {
|
||||||
|
const HIGHLIGHT: f32 = 0.5; /* SCAFFOLDING */
|
||||||
|
let representation = self.representation.get_clone_untracked();
|
||||||
|
let color = if selected { self.color.map(|channel| 0.2 + 0.8*channel) } else { self.color };
|
||||||
|
let highlight = if selected { 1.0 } else { HIGHLIGHT };
|
||||||
|
scene.points.push(representation, color, highlight, selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SCAFFOLDING */
|
||||||
|
fn cast(&self, dir: Vector3<f64>, assembly_to_world: &DMatrix<f64>, pixel_size: f64) -> Option<f64> {
|
||||||
|
let rep = self.representation.with_untracked(|rep| assembly_to_world * rep);
|
||||||
|
if rep[2] < 0.0 {
|
||||||
|
// this constant should be kept synchronized with `point.frag`
|
||||||
|
const POINT_RADIUS_PX: f64 = 4.0;
|
||||||
|
|
||||||
|
// find the radius of the point in screen projection units
|
||||||
|
let point_radius_proj = POINT_RADIUS_PX * pixel_size;
|
||||||
|
|
||||||
|
// find the squared distance between the screen projections of the
|
||||||
|
// ray and the point
|
||||||
|
let dir_proj = -dir.fixed_rows::<2>(0) / dir[2];
|
||||||
|
let rep_proj = -rep.fixed_rows::<2>(0) / rep[2];
|
||||||
|
let dist_sq = (dir_proj - rep_proj).norm_squared();
|
||||||
|
|
||||||
|
// if the ray hits the point, return its depth
|
||||||
|
if dist_sq < point_radius_proj * point_radius_proj {
|
||||||
|
Some(rep[2] / dir[2])
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- WebGL utilities ---
|
||||||
|
|
||||||
fn compile_shader(
|
fn compile_shader(
|
||||||
context: &WebGl2RenderingContext,
|
context: &WebGl2RenderingContext,
|
||||||
|
@ -27,6 +195,45 @@ fn compile_shader(
|
||||||
shader
|
shader
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_up_program(
|
||||||
|
context: &WebGl2RenderingContext,
|
||||||
|
vertex_shader_source: &str,
|
||||||
|
fragment_shader_source: &str
|
||||||
|
) -> WebGlProgram {
|
||||||
|
// compile the shaders
|
||||||
|
let vertex_shader = compile_shader(
|
||||||
|
&context,
|
||||||
|
WebGl2RenderingContext::VERTEX_SHADER,
|
||||||
|
vertex_shader_source,
|
||||||
|
);
|
||||||
|
let fragment_shader = compile_shader(
|
||||||
|
&context,
|
||||||
|
WebGl2RenderingContext::FRAGMENT_SHADER,
|
||||||
|
fragment_shader_source,
|
||||||
|
);
|
||||||
|
|
||||||
|
// create the program and attach the shaders
|
||||||
|
let program = context.create_program().unwrap();
|
||||||
|
context.attach_shader(&program, &vertex_shader);
|
||||||
|
context.attach_shader(&program, &fragment_shader);
|
||||||
|
context.link_program(&program);
|
||||||
|
|
||||||
|
/* DEBUG */
|
||||||
|
// report whether linking succeeded
|
||||||
|
let link_status = context
|
||||||
|
.get_program_parameter(&program, WebGl2RenderingContext::LINK_STATUS)
|
||||||
|
.as_bool()
|
||||||
|
.unwrap();
|
||||||
|
let link_msg = if link_status {
|
||||||
|
"Linked successfully"
|
||||||
|
} else {
|
||||||
|
"Linking failed"
|
||||||
|
};
|
||||||
|
console::log_1(&JsValue::from(link_msg));
|
||||||
|
|
||||||
|
program
|
||||||
|
}
|
||||||
|
|
||||||
fn get_uniform_array_locations<const N: usize>(
|
fn get_uniform_array_locations<const N: usize>(
|
||||||
context: &WebGl2RenderingContext,
|
context: &WebGl2RenderingContext,
|
||||||
program: &WebGlProgram,
|
program: &WebGlProgram,
|
||||||
|
@ -42,22 +249,39 @@ fn get_uniform_array_locations<const N: usize>(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// load the given data into the vertex input of the given name
|
// bind the given vertex buffer object to the given vertex attribute
|
||||||
fn bind_vertex_attrib(
|
fn bind_to_attribute(
|
||||||
context: &WebGl2RenderingContext,
|
context: &WebGl2RenderingContext,
|
||||||
index: u32,
|
attr_index: u32,
|
||||||
size: i32,
|
attr_size: i32,
|
||||||
data: &[f32]
|
buffer: &Option<WebGlBuffer>
|
||||||
) {
|
) {
|
||||||
// create a data buffer and bind it to ARRAY_BUFFER
|
context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, buffer.as_ref());
|
||||||
let buffer = context.create_buffer().unwrap();
|
context.vertex_attrib_pointer_with_i32(
|
||||||
context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, Some(&buffer));
|
attr_index,
|
||||||
|
attr_size,
|
||||||
|
WebGl2RenderingContext::FLOAT,
|
||||||
|
false, // don't normalize
|
||||||
|
0, // zero stride
|
||||||
|
0, // zero offset
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// load the given data into a new vertex buffer object
|
||||||
|
fn load_new_buffer(
|
||||||
|
context: &WebGl2RenderingContext,
|
||||||
|
data: &[f32]
|
||||||
|
) -> Option<WebGlBuffer> {
|
||||||
|
// create a buffer and bind it to ARRAY_BUFFER
|
||||||
|
let buffer = context.create_buffer();
|
||||||
|
context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, buffer.as_ref());
|
||||||
|
|
||||||
// load the given data into the buffer. the function `Float32Array::view`
|
// load the given data into the buffer. this block is unsafe because
|
||||||
// creates a raw view into our module's `WebAssembly.Memory` buffer.
|
// `Float32Array::view` creates a raw view into our module's
|
||||||
// allocating more memory will change the buffer, invalidating the view.
|
// `WebAssembly.Memory` buffer. allocating more memory will change the
|
||||||
// that means we have to make sure we don't allocate any memory until the
|
// buffer, invalidating the view, so we have to make sure we don't allocate
|
||||||
// view is dropped
|
// any memory until the view is dropped. we're okay here because the view is
|
||||||
|
// used as soon as it's created
|
||||||
unsafe {
|
unsafe {
|
||||||
context.buffer_data_with_array_buffer_view(
|
context.buffer_data_with_array_buffer_view(
|
||||||
WebGl2RenderingContext::ARRAY_BUFFER,
|
WebGl2RenderingContext::ARRAY_BUFFER,
|
||||||
|
@ -66,42 +290,43 @@ fn bind_vertex_attrib(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// allow the target attribute to be used
|
buffer
|
||||||
context.enable_vertex_attrib_array(index);
|
}
|
||||||
|
|
||||||
// take whatever's bound to ARRAY_BUFFER---here, the data buffer created
|
fn bind_new_buffer_to_attribute(
|
||||||
// above---and bind it to the target attribute
|
context: &WebGl2RenderingContext,
|
||||||
//
|
attr_index: u32,
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/vertexAttribPointer
|
attr_size: i32,
|
||||||
//
|
data: &[f32]
|
||||||
context.vertex_attrib_pointer_with_i32(
|
) {
|
||||||
index,
|
let buffer = load_new_buffer(context, data);
|
||||||
size,
|
bind_to_attribute(context, attr_index, attr_size, &buffer);
|
||||||
WebGl2RenderingContext::FLOAT,
|
|
||||||
false, // don't normalize
|
|
||||||
0, // zero stride
|
|
||||||
0, // zero offset
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// the direction in camera space that a mouse event is pointing along
|
// the direction in camera space that a mouse event is pointing along
|
||||||
fn event_dir(event: &MouseEvent) -> Vector3<f64> {
|
fn event_dir(event: &MouseEvent) -> (Vector3<f64>, f64) {
|
||||||
let target: Element = event.target().unwrap().unchecked_into();
|
let target: web_sys::Element = event.target().unwrap().unchecked_into();
|
||||||
let rect = target.get_bounding_client_rect();
|
let rect = target.get_bounding_client_rect();
|
||||||
let width = rect.width();
|
let width = rect.width();
|
||||||
let height = rect.height();
|
let height = rect.height();
|
||||||
let shortdim = width.min(height);
|
let shortdim = width.min(height);
|
||||||
|
|
||||||
// this constant should be kept synchronized with `inversive.frag`
|
// this constant should be kept synchronized with `spheres.frag` and
|
||||||
|
// `point.vert`
|
||||||
const FOCAL_SLOPE: f64 = 0.3;
|
const FOCAL_SLOPE: f64 = 0.3;
|
||||||
|
|
||||||
Vector3::new(
|
(
|
||||||
FOCAL_SLOPE * (2.0*(f64::from(event.client_x()) - rect.left()) - width) / shortdim,
|
Vector3::new(
|
||||||
FOCAL_SLOPE * (2.0*(rect.bottom() - f64::from(event.client_y())) - height) / shortdim,
|
FOCAL_SLOPE * (2.0*(f64::from(event.client_x()) - rect.left()) - width) / shortdim,
|
||||||
-1.0
|
FOCAL_SLOPE * (2.0*(rect.bottom() - f64::from(event.client_y())) - height) / shortdim,
|
||||||
|
-1.0
|
||||||
|
),
|
||||||
|
FOCAL_SLOPE * 2.0 / shortdim
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- display component ---
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Display() -> View {
|
pub fn Display() -> View {
|
||||||
let state = use_context::<AppState>();
|
let state = use_context::<AppState>();
|
||||||
|
@ -138,7 +363,7 @@ pub fn Display() -> View {
|
||||||
create_effect(move || {
|
create_effect(move || {
|
||||||
state.assembly.elements.with(|elts| {
|
state.assembly.elements.with(|elts| {
|
||||||
for (_, elt) in elts {
|
for (_, elt) in elts {
|
||||||
elt.representation.track();
|
elt.representation().track();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
state.selection.track();
|
state.selection.track();
|
||||||
|
@ -170,7 +395,6 @@ pub fn Display() -> View {
|
||||||
|
|
||||||
// display parameters
|
// display parameters
|
||||||
const OPACITY: f32 = 0.5; /* SCAFFOLDING */
|
const OPACITY: f32 = 0.5; /* SCAFFOLDING */
|
||||||
const HIGHLIGHT: f32 = 0.2; /* SCAFFOLDING */
|
|
||||||
const LAYER_THRESHOLD: i32 = 0; /* DEBUG */
|
const LAYER_THRESHOLD: i32 = 0; /* DEBUG */
|
||||||
const DEBUG_MODE: i32 = 0; /* DEBUG */
|
const DEBUG_MODE: i32 = 0; /* DEBUG */
|
||||||
|
|
||||||
|
@ -186,32 +410,26 @@ pub fn Display() -> View {
|
||||||
.dyn_into::<WebGl2RenderingContext>()
|
.dyn_into::<WebGl2RenderingContext>()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// compile and attach the vertex and fragment shaders
|
// disable depth testing
|
||||||
let vertex_shader = compile_shader(
|
ctx.disable(WebGl2RenderingContext::DEPTH_TEST);
|
||||||
|
|
||||||
|
// set blend mode
|
||||||
|
ctx.enable(WebGl2RenderingContext::BLEND);
|
||||||
|
ctx.blend_func(WebGl2RenderingContext::SRC_ALPHA, WebGl2RenderingContext::ONE_MINUS_SRC_ALPHA);
|
||||||
|
|
||||||
|
// set up the sphere rendering program
|
||||||
|
let sphere_program = set_up_program(
|
||||||
&ctx,
|
&ctx,
|
||||||
WebGl2RenderingContext::VERTEX_SHADER,
|
|
||||||
include_str!("identity.vert"),
|
include_str!("identity.vert"),
|
||||||
|
include_str!("spheres.frag")
|
||||||
);
|
);
|
||||||
let fragment_shader = compile_shader(
|
|
||||||
|
// set up the point rendering program
|
||||||
|
let point_program = set_up_program(
|
||||||
&ctx,
|
&ctx,
|
||||||
WebGl2RenderingContext::FRAGMENT_SHADER,
|
include_str!("point.vert"),
|
||||||
include_str!("inversive.frag"),
|
include_str!("point.frag")
|
||||||
);
|
);
|
||||||
let program = ctx.create_program().unwrap();
|
|
||||||
ctx.attach_shader(&program, &vertex_shader);
|
|
||||||
ctx.attach_shader(&program, &fragment_shader);
|
|
||||||
ctx.link_program(&program);
|
|
||||||
let link_status = ctx
|
|
||||||
.get_program_parameter(&program, WebGl2RenderingContext::LINK_STATUS)
|
|
||||||
.as_bool()
|
|
||||||
.unwrap();
|
|
||||||
let link_msg = if link_status {
|
|
||||||
"Linked successfully"
|
|
||||||
} else {
|
|
||||||
"Linking failed"
|
|
||||||
};
|
|
||||||
console::log_1(&JsValue::from(link_msg));
|
|
||||||
ctx.use_program(Some(&program));
|
|
||||||
|
|
||||||
/* DEBUG */
|
/* DEBUG */
|
||||||
// print the maximum number of vectors that can be passed as
|
// print the maximum number of vectors that can be passed as
|
||||||
|
@ -230,35 +448,33 @@ pub fn Display() -> View {
|
||||||
&JsValue::from("uniform vectors available")
|
&JsValue::from("uniform vectors available")
|
||||||
);
|
);
|
||||||
|
|
||||||
// find indices of vertex attributes and uniforms
|
// find the sphere program's vertex attribute
|
||||||
|
let viewport_position_attr = ctx.get_attrib_location(&sphere_program, "position") as u32;
|
||||||
|
|
||||||
|
// find the sphere program's uniforms
|
||||||
const SPHERE_MAX: usize = 200;
|
const SPHERE_MAX: usize = 200;
|
||||||
let position_index = ctx.get_attrib_location(&program, "position") as u32;
|
let sphere_cnt_loc = ctx.get_uniform_location(&sphere_program, "sphere_cnt");
|
||||||
let sphere_cnt_loc = ctx.get_uniform_location(&program, "sphere_cnt");
|
|
||||||
let sphere_sp_locs = get_uniform_array_locations::<SPHERE_MAX>(
|
let sphere_sp_locs = get_uniform_array_locations::<SPHERE_MAX>(
|
||||||
&ctx, &program, "sphere_list", Some("sp")
|
&ctx, &sphere_program, "sphere_list", Some("sp")
|
||||||
);
|
);
|
||||||
let sphere_lt_locs = get_uniform_array_locations::<SPHERE_MAX>(
|
let sphere_lt_locs = get_uniform_array_locations::<SPHERE_MAX>(
|
||||||
&ctx, &program, "sphere_list", Some("lt")
|
&ctx, &sphere_program, "sphere_list", Some("lt")
|
||||||
);
|
);
|
||||||
let color_locs = get_uniform_array_locations::<SPHERE_MAX>(
|
let sphere_color_locs = get_uniform_array_locations::<SPHERE_MAX>(
|
||||||
&ctx, &program, "color_list", None
|
&ctx, &sphere_program, "color_list", None
|
||||||
);
|
);
|
||||||
let highlight_locs = get_uniform_array_locations::<SPHERE_MAX>(
|
let sphere_highlight_locs = get_uniform_array_locations::<SPHERE_MAX>(
|
||||||
&ctx, &program, "highlight_list", None
|
&ctx, &sphere_program, "highlight_list", None
|
||||||
);
|
);
|
||||||
let resolution_loc = ctx.get_uniform_location(&program, "resolution");
|
let resolution_loc = ctx.get_uniform_location(&sphere_program, "resolution");
|
||||||
let shortdim_loc = ctx.get_uniform_location(&program, "shortdim");
|
let shortdim_loc = ctx.get_uniform_location(&sphere_program, "shortdim");
|
||||||
let opacity_loc = ctx.get_uniform_location(&program, "opacity");
|
let opacity_loc = ctx.get_uniform_location(&sphere_program, "opacity");
|
||||||
let layer_threshold_loc = ctx.get_uniform_location(&program, "layer_threshold");
|
let layer_threshold_loc = ctx.get_uniform_location(&sphere_program, "layer_threshold");
|
||||||
let debug_mode_loc = ctx.get_uniform_location(&program, "debug_mode");
|
let debug_mode_loc = ctx.get_uniform_location(&sphere_program, "debug_mode");
|
||||||
|
|
||||||
// create a vertex array and bind it to the graphics context
|
// load the viewport vertex positions into a new vertex buffer object
|
||||||
let vertex_array = ctx.create_vertex_array().unwrap();
|
|
||||||
ctx.bind_vertex_array(Some(&vertex_array));
|
|
||||||
|
|
||||||
// set the vertex positions
|
|
||||||
const VERTEX_CNT: usize = 6;
|
const VERTEX_CNT: usize = 6;
|
||||||
let positions: [f32; 3*VERTEX_CNT] = [
|
let viewport_positions: [f32; 3*VERTEX_CNT] = [
|
||||||
// northwest triangle
|
// northwest triangle
|
||||||
-1.0, -1.0, 0.0,
|
-1.0, -1.0, 0.0,
|
||||||
-1.0, 1.0, 0.0,
|
-1.0, 1.0, 0.0,
|
||||||
|
@ -268,7 +484,13 @@ pub fn Display() -> View {
|
||||||
1.0, 1.0, 0.0,
|
1.0, 1.0, 0.0,
|
||||||
1.0, -1.0, 0.0
|
1.0, -1.0, 0.0
|
||||||
];
|
];
|
||||||
bind_vertex_attrib(&ctx, position_index, 3, &positions);
|
let viewport_position_buffer = load_new_buffer(&ctx, &viewport_positions);
|
||||||
|
|
||||||
|
// find the point program's vertex attributes
|
||||||
|
let point_position_attr = ctx.get_attrib_location(&point_program, "position") as u32;
|
||||||
|
let point_color_attr = ctx.get_attrib_location(&point_program, "color") as u32;
|
||||||
|
let point_highlight_attr = ctx.get_attrib_location(&point_program, "highlight") as u32;
|
||||||
|
let point_selection_attr = ctx.get_attrib_location(&point_program, "selected") as u32;
|
||||||
|
|
||||||
// set up a repainting routine
|
// set up a repainting routine
|
||||||
let (_, start_animation_loop, _) = create_raf(move || {
|
let (_, start_animation_loop, _) = create_raf(move || {
|
||||||
|
@ -362,6 +584,9 @@ pub fn Display() -> View {
|
||||||
}
|
}
|
||||||
|
|
||||||
if scene_changed.get() {
|
if scene_changed.get() {
|
||||||
|
const SPACE_DIM: usize = 3;
|
||||||
|
const COLOR_SIZE: usize = 3;
|
||||||
|
|
||||||
/* INSTRUMENTS */
|
/* INSTRUMENTS */
|
||||||
// measure mean frame interval
|
// measure mean frame interval
|
||||||
frames_since_last_sample += 1;
|
frames_since_last_sample += 1;
|
||||||
|
@ -371,6 +596,10 @@ pub fn Display() -> View {
|
||||||
frames_since_last_sample = 0;
|
frames_since_last_sample = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- get the assembly ---
|
||||||
|
|
||||||
|
let mut scene = Scene::new();
|
||||||
|
|
||||||
// find the map from assembly space to world space
|
// find the map from assembly space to world space
|
||||||
let location = {
|
let location = {
|
||||||
let u = -location_z;
|
let u = -location_z;
|
||||||
|
@ -384,41 +613,27 @@ pub fn Display() -> View {
|
||||||
};
|
};
|
||||||
let asm_to_world = &location * &orientation;
|
let asm_to_world = &location * &orientation;
|
||||||
|
|
||||||
// get the assembly
|
// set up the scene
|
||||||
let (
|
state.assembly.elements.with_untracked(
|
||||||
elt_cnt,
|
|elts| for (key, elt) in elts {
|
||||||
reps_world,
|
let selected = state.selection.with(|sel| sel.contains(&key));
|
||||||
colors,
|
elt.show(&mut scene, selected);
|
||||||
highlights
|
}
|
||||||
) = state.assembly.elements.with(|elts| {
|
);
|
||||||
(
|
let sphere_cnt = scene.spheres.len_i32();
|
||||||
// number of elements
|
|
||||||
elts.len() as i32,
|
// --- draw the spheres ---
|
||||||
|
|
||||||
// representation vectors in world coordinates
|
// use the sphere rendering program
|
||||||
elts.iter().map(
|
ctx.use_program(Some(&sphere_program));
|
||||||
|(_, elt)| elt.representation.with(|rep| &asm_to_world * rep)
|
|
||||||
).collect::<Vec<_>>(),
|
// enable the sphere program's vertex attribute
|
||||||
|
ctx.enable_vertex_attrib_array(viewport_position_attr);
|
||||||
// colors
|
|
||||||
elts.iter().map(|(key, elt)| {
|
// write the spheres in world coordinates
|
||||||
if state.selection.with(|sel| sel.contains(&key)) {
|
let sphere_reps_world: Vec<_> = scene.spheres.representations.into_iter().map(
|
||||||
elt.color.map(|ch| 0.2 + 0.8*ch)
|
|rep| (&asm_to_world * rep).cast::<f32>()
|
||||||
} else {
|
).collect();
|
||||||
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;
|
||||||
|
@ -426,25 +641,25 @@ pub fn Display() -> View {
|
||||||
ctx.uniform2f(resolution_loc.as_ref(), width, height);
|
ctx.uniform2f(resolution_loc.as_ref(), width, height);
|
||||||
ctx.uniform1f(shortdim_loc.as_ref(), width.min(height));
|
ctx.uniform1f(shortdim_loc.as_ref(), width.min(height));
|
||||||
|
|
||||||
// pass the assembly
|
// pass the scene data
|
||||||
ctx.uniform1i(sphere_cnt_loc.as_ref(), elt_cnt);
|
ctx.uniform1i(sphere_cnt_loc.as_ref(), sphere_cnt);
|
||||||
for n in 0..reps_world.len() {
|
for n in 0..sphere_reps_world.len() {
|
||||||
let v = &reps_world[n];
|
let v = &sphere_reps_world[n];
|
||||||
ctx.uniform3f(
|
ctx.uniform3fv_with_f32_array(
|
||||||
sphere_sp_locs[n].as_ref(),
|
sphere_sp_locs[n].as_ref(),
|
||||||
v[0] as f32, v[1] as f32, v[2] as f32
|
v.rows(0, 3).as_slice()
|
||||||
);
|
);
|
||||||
ctx.uniform2f(
|
ctx.uniform2fv_with_f32_array(
|
||||||
sphere_lt_locs[n].as_ref(),
|
sphere_lt_locs[n].as_ref(),
|
||||||
v[3] as f32, v[4] as f32
|
v.rows(3, 2).as_slice()
|
||||||
);
|
);
|
||||||
ctx.uniform3fv_with_f32_array(
|
ctx.uniform3fv_with_f32_array(
|
||||||
color_locs[n].as_ref(),
|
sphere_color_locs[n].as_ref(),
|
||||||
&colors[n]
|
&scene.spheres.colors[n]
|
||||||
);
|
);
|
||||||
ctx.uniform1f(
|
ctx.uniform1f(
|
||||||
highlight_locs[n].as_ref(),
|
sphere_highlight_locs[n].as_ref(),
|
||||||
highlights[n]
|
scene.spheres.highlights[n]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -453,9 +668,56 @@ pub fn Display() -> View {
|
||||||
ctx.uniform1i(layer_threshold_loc.as_ref(), LAYER_THRESHOLD);
|
ctx.uniform1i(layer_threshold_loc.as_ref(), LAYER_THRESHOLD);
|
||||||
ctx.uniform1i(debug_mode_loc.as_ref(), DEBUG_MODE);
|
ctx.uniform1i(debug_mode_loc.as_ref(), DEBUG_MODE);
|
||||||
|
|
||||||
|
// bind the viewport vertex position buffer to the position
|
||||||
|
// attribute in the vertex shader
|
||||||
|
bind_to_attribute(&ctx, viewport_position_attr, SPACE_DIM as i32, &viewport_position_buffer);
|
||||||
|
|
||||||
// draw the scene
|
// draw the scene
|
||||||
ctx.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, VERTEX_CNT as i32);
|
ctx.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, VERTEX_CNT as i32);
|
||||||
|
|
||||||
|
// disable the sphere program's vertex attribute
|
||||||
|
ctx.disable_vertex_attrib_array(viewport_position_attr);
|
||||||
|
|
||||||
|
// --- draw the points ---
|
||||||
|
|
||||||
|
if !scene.points.representations.is_empty() {
|
||||||
|
// use the point rendering program
|
||||||
|
ctx.use_program(Some(&point_program));
|
||||||
|
|
||||||
|
// enable the point program's vertex attributes
|
||||||
|
ctx.enable_vertex_attrib_array(point_position_attr);
|
||||||
|
ctx.enable_vertex_attrib_array(point_color_attr);
|
||||||
|
ctx.enable_vertex_attrib_array(point_highlight_attr);
|
||||||
|
ctx.enable_vertex_attrib_array(point_selection_attr);
|
||||||
|
|
||||||
|
// write the points in world coordinates
|
||||||
|
let asm_to_world_sp = asm_to_world.rows(0, SPACE_DIM);
|
||||||
|
let point_positions = DMatrix::from_columns(
|
||||||
|
&scene.points.representations.into_iter().map(
|
||||||
|
|rep| &asm_to_world_sp * rep
|
||||||
|
).collect::<Vec<_>>().as_slice()
|
||||||
|
).cast::<f32>();
|
||||||
|
|
||||||
|
// load the point positions and colors into new buffers and
|
||||||
|
// bind them to the corresponding attributes in the vertex
|
||||||
|
// shader
|
||||||
|
bind_new_buffer_to_attribute(&ctx, point_position_attr, SPACE_DIM as i32, point_positions.as_slice());
|
||||||
|
bind_new_buffer_to_attribute(&ctx, point_color_attr, COLOR_SIZE as i32, scene.points.colors.concat().as_slice());
|
||||||
|
bind_new_buffer_to_attribute(&ctx, point_highlight_attr, 1 as i32, scene.points.highlights.as_slice());
|
||||||
|
bind_new_buffer_to_attribute(&ctx, point_selection_attr, 1 as i32, scene.points.selections.as_slice());
|
||||||
|
|
||||||
|
// draw the scene
|
||||||
|
ctx.draw_arrays(WebGl2RenderingContext::POINTS, 0, point_positions.ncols() as i32);
|
||||||
|
|
||||||
|
// disable the point program's vertex attributes
|
||||||
|
ctx.disable_vertex_attrib_array(point_position_attr);
|
||||||
|
ctx.disable_vertex_attrib_array(point_color_attr);
|
||||||
|
ctx.disable_vertex_attrib_array(point_highlight_attr);
|
||||||
|
ctx.disable_vertex_attrib_array(point_selection_attr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- update the display state ---
|
||||||
|
|
||||||
// update the viewpoint
|
// update the viewpoint
|
||||||
assembly_to_world.set(asm_to_world);
|
assembly_to_world.set(asm_to_world);
|
||||||
|
|
||||||
|
@ -585,11 +847,11 @@ pub fn Display() -> View {
|
||||||
},
|
},
|
||||||
on:click=move |event: MouseEvent| {
|
on:click=move |event: MouseEvent| {
|
||||||
// find the nearest element along the pointer direction
|
// find the nearest element along the pointer direction
|
||||||
let dir = event_dir(&event);
|
let (dir, pixel_size) = event_dir(&event);
|
||||||
console::log_1(&JsValue::from(dir.to_string()));
|
console::log_1(&JsValue::from(dir.to_string()));
|
||||||
let mut clicked: Option<(ElementKey, f64)> = None;
|
let mut clicked: Option<(ElementKey, f64)> = None;
|
||||||
for (key, elt) in state.assembly.elements.get_clone_untracked() {
|
for (key, elt) in state.assembly.elements.get_clone_untracked() {
|
||||||
match assembly_to_world.with(|asm_to_world| elt.cast(dir, asm_to_world)) {
|
match assembly_to_world.with(|asm_to_world| elt.cast(dir, asm_to_world, pixel_size)) {
|
||||||
Some(depth) => match clicked {
|
Some(depth) => match clicked {
|
||||||
Some((_, best_depth)) => {
|
Some((_, best_depth)) => {
|
||||||
if depth < best_depth {
|
if depth < best_depth {
|
||||||
|
|
|
@ -4,7 +4,6 @@ use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */
|
||||||
|
|
||||||
// --- elements ---
|
// --- elements ---
|
||||||
|
|
||||||
#[cfg(feature = "dev")]
|
|
||||||
pub fn point(x: f64, y: f64, z: f64) -> DVector<f64> {
|
pub fn point(x: f64, y: f64, z: f64) -> DVector<f64> {
|
||||||
DVector::from_column_slice(&[x, y, z, 0.5, 0.5*(x*x + y*y + z*z)])
|
DVector::from_column_slice(&[x, y, z, 0.5, 0.5*(x*x + y*y + z*z)])
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,9 +9,10 @@ use web_sys::{
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
AppState,
|
AppState,
|
||||||
assembly,
|
|
||||||
assembly::{
|
assembly::{
|
||||||
|
Element,
|
||||||
ElementKey,
|
ElementKey,
|
||||||
|
ElementRc,
|
||||||
HalfCurvatureRegulator,
|
HalfCurvatureRegulator,
|
||||||
InversiveDistanceRegulator,
|
InversiveDistanceRegulator,
|
||||||
Regulator,
|
Regulator,
|
||||||
|
@ -103,7 +104,7 @@ impl OutlineItem for InversiveDistanceRegulator {
|
||||||
self.subjects[0]
|
self.subjects[0]
|
||||||
};
|
};
|
||||||
let other_subject_label = state.assembly.elements.with(
|
let other_subject_label = state.assembly.elements.with(
|
||||||
|elts| elts[other_subject].label.clone()
|
|elts| elts[other_subject].label().clone()
|
||||||
);
|
);
|
||||||
view! {
|
view! {
|
||||||
li(class="regulator") {
|
li(class="regulator") {
|
||||||
|
@ -141,14 +142,15 @@ fn RegulatorOutlineItem(regulator_key: RegulatorKey, element_key: ElementKey) ->
|
||||||
|
|
||||||
// a list item that shows an element in an outline view of an assembly
|
// a list item that shows an element in an outline view of an assembly
|
||||||
#[component(inline_props)]
|
#[component(inline_props)]
|
||||||
fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View {
|
fn ElementOutlineItem(key: ElementKey, element: Rc<dyn Element>) -> View {
|
||||||
let state = use_context::<AppState>();
|
let state = use_context::<AppState>();
|
||||||
let class = state.selection.map(
|
let class = state.selection.map(
|
||||||
move |sel| if sel.contains(&key) { "selected" } else { "" }
|
move |sel| if sel.contains(&key) { "selected" } else { "" }
|
||||||
);
|
);
|
||||||
let label = element.label.clone();
|
let label = element.label().clone();
|
||||||
|
let representation = element.representation().clone();
|
||||||
let rep_components = move || {
|
let rep_components = move || {
|
||||||
element.representation.with(
|
representation.with(
|
||||||
|rep| rep.iter().map(
|
|rep| rep.iter().map(
|
||||||
|u| {
|
|u| {
|
||||||
let u_str = format!("{:.3}", u).replace("-", "\u{2212}");
|
let u_str = format!("{:.3}", u).replace("-", "\u{2212}");
|
||||||
|
@ -157,8 +159,8 @@ fn ElementOutlineItem(key: ElementKey, element: assembly::Element) -> View {
|
||||||
).collect::<Vec<_>>()
|
).collect::<Vec<_>>()
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
let regulated = element.regulators.map(|regs| regs.len() > 0);
|
let regulated = element.regulators().map(|regs| regs.len() > 0);
|
||||||
let regulator_list = element.regulators.map(
|
let regulator_list = element.regulators().map(
|
||||||
move |elt_reg_keys| elt_reg_keys
|
move |elt_reg_keys| elt_reg_keys
|
||||||
.clone()
|
.clone()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
@ -261,7 +263,8 @@ pub fn Outline() -> View {
|
||||||
|elts| elts
|
|elts| elts
|
||||||
.clone()
|
.clone()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.sorted_by_key(|(_, elt)| elt.id.clone())
|
.sorted_by_key(|(_, elt)| elt.id().clone())
|
||||||
|
.map(|(key, elt)| (key, ElementRc(elt)))
|
||||||
.collect()
|
.collect()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -275,10 +278,10 @@ pub fn Outline() -> View {
|
||||||
) {
|
) {
|
||||||
Keyed(
|
Keyed(
|
||||||
list=element_list,
|
list=element_list,
|
||||||
view=|(key, elt)| view! {
|
view=|(key, ElementRc(elt))| view! {
|
||||||
ElementOutlineItem(key=key, element=elt)
|
ElementOutlineItem(key=key, element=elt)
|
||||||
},
|
},
|
||||||
key=|(_, elt)| elt.serial
|
key=|(_, ElementRc(elt))| elt.serial()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
18
app-proto/src/point.frag
Normal file
18
app-proto/src/point.frag
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
#version 300 es
|
||||||
|
|
||||||
|
precision highp float;
|
||||||
|
|
||||||
|
in vec3 point_color;
|
||||||
|
in float point_highlight;
|
||||||
|
in float total_radius;
|
||||||
|
|
||||||
|
out vec4 outColor;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
float r = total_radius * length(2.*gl_PointCoord - vec2(1.));
|
||||||
|
|
||||||
|
const float POINT_RADIUS = 4.;
|
||||||
|
float border = smoothstep(POINT_RADIUS - 1., POINT_RADIUS, r);
|
||||||
|
vec3 color = mix(point_color, vec3(1.), border * point_highlight);
|
||||||
|
outColor = vec4(color, 1. - smoothstep(total_radius - 1., total_radius, r));
|
||||||
|
}
|
24
app-proto/src/point.vert
Normal file
24
app-proto/src/point.vert
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
#version 300 es
|
||||||
|
|
||||||
|
in vec4 position;
|
||||||
|
in vec3 color;
|
||||||
|
in float highlight;
|
||||||
|
in float selected;
|
||||||
|
|
||||||
|
out vec3 point_color;
|
||||||
|
out float point_highlight;
|
||||||
|
out float total_radius;
|
||||||
|
|
||||||
|
// camera
|
||||||
|
const float focal_slope = 0.3;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
total_radius = 5. + 0.5*selected;
|
||||||
|
|
||||||
|
float depth = -focal_slope * position.z;
|
||||||
|
gl_Position = vec4(position.xy / depth, 0., 1.);
|
||||||
|
gl_PointSize = 2.*total_radius;
|
||||||
|
|
||||||
|
point_color = color;
|
||||||
|
point_highlight = highlight;
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue