Implements regulators for the Euclidean coordinates of Point entities, automatically creating all three of them for each added point entity. When such a regulator is set, it freezes the corresponding representation coordinate to the set point. In addition, if all three coordinates of a given Point are set, the coradius coordinate (which holds the norm of the point) is frozen as well. Note that a PointCoordinateRegulator must be created with a Point as the subject. This commit modifies HalfCurvatureRegulator analogously, so that it can only be created with a Sphere. A couple of prospective issues that should be filed in association with this commit: * The new coordinate regulators create redundant display information with the raw representation coordinates of a point that are already shown in the outline view. * The optimization status of these regulators together with HalfCurvature regulators (i.e., the ones implemented by freezing coordinates) is different from InversiveDistance regulators when an Assembly is unrealizable: the frozen-coordinate constraints will be "hard" in that they will be forced to precisely equal their set point, whereas the distance regulators are "soft" in that they can be relaxed from their set points in an effort to minimize the loss function of the configuration as compared to the values of the constraints. Perhaps at some point we should/will have a mechanism to specify the softness/hardness of constraints, but in the meantime, there should not be two different categories of constraints. Suppose we decide that by default that all constraints are soft. Then the optimizer should be able to search changing, for example, the radius of a curvature-constrained sphere, so as to minimize the loss function (for a loss that would therefore presumably have a term akin to the square of the difference between the specified and actual half-curvature of the sphere). For example, suppose you specify that the half-curvature of a sphere is 1 (so it has radius 1/2) but that its distance to a point is -1. These constraints cannot be satisfied, so the optimization fails, presumably with the point at the sphere center, and the sphere with radius 1/2. So all of the loss is concentrated in the difference between the actual point-sphere distance being -1/2, not -1. It would be more appropriate (in the all-soft constraint regime) to end up at something like a sphere of half-curvature 1/√2 with the point at the center, so that the loss is split between both the half-curvature and the distance to the sphere being off by 1 - 1/√2. (At a guess, that would minimize the sum of the squares of the two differences.)
275 lines
No EOL
9.3 KiB
Rust
275 lines
No EOL
9.3 KiB
Rust
use itertools::Itertools;
|
|
use std::rc::Rc;
|
|
use sycamore::prelude::*;
|
|
use web_sys::{KeyboardEvent, MouseEvent, wasm_bindgen::JsCast};
|
|
|
|
use crate::{
|
|
AppState,
|
|
assembly::{
|
|
Axis,
|
|
Element,
|
|
HalfCurvatureRegulator,
|
|
InversiveDistanceRegulator,
|
|
PointCoordinateRegulator,
|
|
Regulator,
|
|
},
|
|
specified::SpecifiedValue
|
|
};
|
|
|
|
// an editable view of a regulator
|
|
#[component(inline_props)]
|
|
fn RegulatorInput(regulator: Rc<dyn Regulator>) -> View {
|
|
// get the regulator's measurement and set point signals
|
|
let measurement = regulator.measurement();
|
|
let set_point = regulator.set_point();
|
|
|
|
// the `valid` signal tracks whether the last entered value is a valid set
|
|
// point specification
|
|
let valid = create_signal(true);
|
|
|
|
// the `value` signal holds the current set point specification
|
|
let value = create_signal(
|
|
set_point.with_untracked(|set_pt| set_pt.spec.clone())
|
|
);
|
|
|
|
// this `reset_value` closure resets the input value to the regulator's set
|
|
// point specification
|
|
let reset_value = move || {
|
|
batch(|| {
|
|
valid.set(true);
|
|
value.set(set_point.with(|set_pt| set_pt.spec.clone()));
|
|
})
|
|
};
|
|
|
|
// reset the input value whenever the regulator's set point specification
|
|
// is updated
|
|
create_effect(reset_value);
|
|
|
|
view! {
|
|
input(
|
|
r#type = "text",
|
|
class = move || {
|
|
if valid.get() {
|
|
set_point.with(|set_pt| {
|
|
if set_pt.is_present() {
|
|
"regulator-input constraint"
|
|
} else {
|
|
"regulator-input"
|
|
}
|
|
})
|
|
} else {
|
|
"regulator-input invalid"
|
|
}
|
|
},
|
|
placeholder = measurement.with(|result| result.to_string()),
|
|
bind:value = value,
|
|
on:change = move |_| {
|
|
valid.set(
|
|
match SpecifiedValue::try_from(value.get_clone_untracked()) {
|
|
Ok(set_pt) => {
|
|
set_point.set(set_pt);
|
|
true
|
|
},
|
|
Err(_) => false,
|
|
}
|
|
)
|
|
},
|
|
on:keydown = {
|
|
move |event: KeyboardEvent| {
|
|
match event.key().as_str() {
|
|
"Escape" => reset_value(),
|
|
_ => (),
|
|
}
|
|
}
|
|
},
|
|
)
|
|
}
|
|
}
|
|
|
|
pub trait OutlineItem {
|
|
fn outline_item(self: Rc<Self>, element: &Rc<dyn Element>) -> View;
|
|
}
|
|
|
|
impl OutlineItem for InversiveDistanceRegulator {
|
|
fn outline_item(self: Rc<Self>, element: &Rc<dyn Element>) -> View {
|
|
let other_subject_label = if self.subjects[0] == element.clone() {
|
|
self.subjects[1].label()
|
|
} else {
|
|
self.subjects[0].label()
|
|
}.clone();
|
|
view! {
|
|
li(class = "regulator") {
|
|
div(class = "regulator-label") { (other_subject_label) }
|
|
div(class = "regulator-type") { "Inversive distance" }
|
|
RegulatorInput(regulator = self)
|
|
div(class = "status")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl OutlineItem for HalfCurvatureRegulator {
|
|
fn outline_item(self: Rc<Self>, _element: &Rc<dyn Element>) -> View {
|
|
view! {
|
|
li(class = "regulator") {
|
|
div(class = "regulator-label") // for spacing
|
|
div(class = "regulator-type") { "Half-curvature" }
|
|
RegulatorInput(regulator = self)
|
|
div(class = "status")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl OutlineItem for PointCoordinateRegulator {
|
|
fn outline_item(self: Rc<Self>, _element: &Rc<dyn Element>) -> View {
|
|
view! {
|
|
li(class = "regulator") {
|
|
div(class = "regulator-label") { (Axis::NAME[self.axis as usize]) }
|
|
div(class = "regulator-type") { "Coordinate" }
|
|
RegulatorInput(regulator = self)
|
|
div(class = "status")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// a list item that shows an element in an outline view of an assembly
|
|
#[component(inline_props)]
|
|
fn ElementOutlineItem(element: Rc<dyn Element>) -> View {
|
|
let state = use_context::<AppState>();
|
|
let class = {
|
|
let element_for_class = element.clone();
|
|
state.selection.map(
|
|
move |sel| if sel.contains(&element_for_class) { "selected" } else { "" }
|
|
)
|
|
};
|
|
let label = element.label().clone();
|
|
let representation = element.representation().clone();
|
|
let rep_components = move || {
|
|
representation.with(
|
|
|rep| rep.iter().map(
|
|
|u| {
|
|
let u_str = format!("{:.3}", u).replace("-", "\u{2212}");
|
|
view! { div { (u_str) } }
|
|
}
|
|
).collect::<Vec<_>>()
|
|
)
|
|
};
|
|
let regulated = element.regulators().map(|regs| regs.len() > 0);
|
|
let regulator_list = element.regulators().map(
|
|
|regs| regs
|
|
.clone()
|
|
.into_iter()
|
|
.sorted_by_key(|reg| reg.subjects().len())
|
|
.collect::<Vec<_>>()
|
|
);
|
|
let details_node = create_node_ref();
|
|
view! {
|
|
li {
|
|
details(ref = details_node) {
|
|
summary(
|
|
class = class.get(),
|
|
on:keydown = {
|
|
let element_for_handler = element.clone();
|
|
move |event: KeyboardEvent| {
|
|
match event.key().as_str() {
|
|
"Enter" => {
|
|
state.select(&element_for_handler, event.shift_key());
|
|
event.prevent_default();
|
|
},
|
|
"ArrowRight" if regulated.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 = {
|
|
let state_for_handler = state.clone();
|
|
let element_for_handler = element.clone();
|
|
move |event: MouseEvent| {
|
|
state_for_handler.select(&element_for_handler, event.shift_key());
|
|
event.stop_propagation();
|
|
event.prevent_default();
|
|
}
|
|
}
|
|
) {
|
|
div(class = "element-label") { (label) }
|
|
div(class = "element-representation") { (rep_components) }
|
|
input(
|
|
r#type = "checkbox",
|
|
bind:checked = element.ghost(),
|
|
on:click = |event: MouseEvent| event.stop_propagation()
|
|
)
|
|
}
|
|
}
|
|
ul(class = "regulators") {
|
|
Keyed(
|
|
list = regulator_list,
|
|
view = move |reg| reg.outline_item(&element),
|
|
key = |reg| reg.serial()
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// a component that lists the elements of the current assembly, showing each
|
|
// element's regulators in a collapsible sub-list. its implementation is based
|
|
// on Kate Morley's HTML + CSS tree views:
|
|
//
|
|
// https://iamkate.com/code/tree-views/
|
|
//
|
|
#[component]
|
|
pub fn Outline() -> View {
|
|
let state = use_context::<AppState>();
|
|
|
|
// list the elements alphabetically by ID
|
|
/* TO DO */
|
|
// this code is designed to generalize easily to other sort keys. if we only
|
|
// ever wanted to sort by ID, we could do that more simply using the
|
|
// `elements_by_id` index
|
|
let element_list = state.assembly.elements.map(
|
|
|elts| elts
|
|
.clone()
|
|
.into_iter()
|
|
.sorted_by_key(|elt| elt.id().clone())
|
|
.collect::<Vec<_>>()
|
|
);
|
|
|
|
view! {
|
|
ul(
|
|
id = "outline",
|
|
on:click = {
|
|
let state = use_context::<AppState>();
|
|
move |_| state.selection.update(|sel| sel.clear())
|
|
}
|
|
) {
|
|
Keyed(
|
|
list = element_list,
|
|
view = |elt| view! {
|
|
ElementOutlineItem(element = elt)
|
|
},
|
|
key = |elt| elt.serial()
|
|
)
|
|
}
|
|
}
|
|
} |