forked from StudioInfinity/dyna3
Use pointers, not keys, to refer to regulators
In the process, move the code that used to handle serial numbering for elements into the `Serial` trait, where it can provide serial numbers for regulators too.
This commit is contained in:
parent
fbd6177a07
commit
8a86038de0
3 changed files with 106 additions and 80 deletions
|
@ -3,6 +3,7 @@ name = "dyna3"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
authors = ["Aaron Fenyes", "Glen Whitney"]
|
authors = ["Aaron Fenyes", "Glen Whitney"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
rust-version = "1.86"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["console_error_panic_hook"]
|
default = ["console_error_panic_hook"]
|
||||||
|
|
|
@ -4,7 +4,6 @@ use slab::Slab;
|
||||||
use std::{
|
use std::{
|
||||||
any::{Any, TypeId},
|
any::{Any, TypeId},
|
||||||
cell::Cell,
|
cell::Cell,
|
||||||
collections::BTreeSet,
|
|
||||||
fmt,
|
fmt,
|
||||||
fmt::{Debug, Formatter},
|
fmt::{Debug, Formatter},
|
||||||
hash::{Hash, Hasher},
|
hash::{Hash, Hasher},
|
||||||
|
@ -30,24 +29,55 @@ use crate::{
|
||||||
specified::SpecifiedValue
|
specified::SpecifiedValue
|
||||||
};
|
};
|
||||||
|
|
||||||
// the types of the keys we use to access an assembly's elements and regulators
|
// the types of the keys we use to access an assembly's elements
|
||||||
pub type ElementKey = usize;
|
pub type ElementKey = usize;
|
||||||
pub type RegulatorKey = usize;
|
|
||||||
|
|
||||||
pub type ElementColor = [f32; 3];
|
pub type ElementColor = [f32; 3];
|
||||||
|
|
||||||
/* KLUDGE */
|
/* KLUDGE */
|
||||||
// we should reconsider this design when we build a system for switching between
|
// we should reconsider this design when we build a system for switching between
|
||||||
// assemblies. at that point, we might want to switch to hierarchical keys,
|
// assemblies. at that point, we might want to switch to hierarchical keys,
|
||||||
// where each each element has a key that identifies it within its assembly and
|
// where each each item has a key that identifies it within its assembly and
|
||||||
// each assembly has a key that identifies it within the sesssion
|
// each assembly has a key that identifies it within the sesssion
|
||||||
static NEXT_ELEMENT_SERIAL: AtomicU64 = AtomicU64::new(0);
|
static NEXT_SERIAL: AtomicU64 = AtomicU64::new(0);
|
||||||
|
|
||||||
|
pub trait Serial {
|
||||||
|
// 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_SERIAL.fetch_update(
|
||||||
|
Ordering::SeqCst, Ordering::SeqCst,
|
||||||
|
|serial| serial.checked_add(1)
|
||||||
|
).expect("Out of serial numbers for elements")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hash for dyn Serial {
|
||||||
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
|
self.serial().hash(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for dyn Serial {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.serial() == other.serial()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for dyn Serial {}
|
||||||
|
|
||||||
pub trait ProblemPoser {
|
pub trait ProblemPoser {
|
||||||
fn pose(&self, problem: &mut ConstraintProblem);
|
fn pose(&self, problem: &mut ConstraintProblem);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Element: ProblemPoser + DisplayItem {
|
pub trait Element: Serial + ProblemPoser + DisplayItem {
|
||||||
// the default identifier for an element of this type
|
// the default identifier for an element of this type
|
||||||
fn default_id() -> String where Self: Sized;
|
fn default_id() -> String where Self: Sized;
|
||||||
|
|
||||||
|
@ -65,23 +95,7 @@ pub trait Element: ProblemPoser + DisplayItem {
|
||||||
|
|
||||||
// the regulators the element is subject to. the assembly that owns the
|
// the regulators the element is subject to. the assembly that owns the
|
||||||
// element is responsible for keeping this set up to date
|
// element is responsible for keeping this set up to date
|
||||||
fn regulators(&self) -> Signal<BTreeSet<RegulatorKey>>;
|
fn regulators(&self) -> Signal<Vec<Rc<dyn Regulator>>>;
|
||||||
|
|
||||||
// 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
|
// the configuration matrix column index that was assigned to the element
|
||||||
// last time the assembly was realized, or `None` if the element has never
|
// last time the assembly was realized, or `None` if the element has never
|
||||||
|
@ -102,13 +116,13 @@ impl Debug for dyn Element {
|
||||||
|
|
||||||
impl Hash for dyn Element {
|
impl Hash for dyn Element {
|
||||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
self.serial().hash(state)
|
<dyn Serial>::hash(self, state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for dyn Element {
|
impl PartialEq for dyn Element {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
self.serial() == other.serial()
|
<dyn Serial>::eq(self, other)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,8 +133,8 @@ pub struct Sphere {
|
||||||
pub label: String,
|
pub label: String,
|
||||||
pub color: ElementColor,
|
pub color: ElementColor,
|
||||||
pub representation: Signal<DVector<f64>>,
|
pub representation: Signal<DVector<f64>>,
|
||||||
pub regulators: Signal<BTreeSet<RegulatorKey>>,
|
pub regulators: Signal<Vec<Rc<dyn Regulator>>>,
|
||||||
pub serial: u64,
|
serial: u64,
|
||||||
column_index: Cell<Option<usize>>
|
column_index: Cell<Option<usize>>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,7 +152,7 @@ impl Sphere {
|
||||||
label: label,
|
label: label,
|
||||||
color: color,
|
color: color,
|
||||||
representation: create_signal(representation),
|
representation: create_signal(representation),
|
||||||
regulators: create_signal(BTreeSet::default()),
|
regulators: create_signal(Vec::new()),
|
||||||
serial: Self::next_serial(),
|
serial: Self::next_serial(),
|
||||||
column_index: None.into()
|
column_index: None.into()
|
||||||
}
|
}
|
||||||
|
@ -175,14 +189,10 @@ impl Element for Sphere {
|
||||||
self.representation
|
self.representation
|
||||||
}
|
}
|
||||||
|
|
||||||
fn regulators(&self) -> Signal<BTreeSet<RegulatorKey>> {
|
fn regulators(&self) -> Signal<Vec<Rc<dyn Regulator>>> {
|
||||||
self.regulators
|
self.regulators
|
||||||
}
|
}
|
||||||
|
|
||||||
fn serial(&self) -> u64 {
|
|
||||||
self.serial
|
|
||||||
}
|
|
||||||
|
|
||||||
fn column_index(&self) -> Option<usize> {
|
fn column_index(&self) -> Option<usize> {
|
||||||
self.column_index.get()
|
self.column_index.get()
|
||||||
}
|
}
|
||||||
|
@ -192,6 +202,12 @@ impl Element for Sphere {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Serial for Sphere {
|
||||||
|
fn serial(&self) -> u64 {
|
||||||
|
self.serial
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ProblemPoser for Sphere {
|
impl ProblemPoser for Sphere {
|
||||||
fn pose(&self, problem: &mut ConstraintProblem) {
|
fn pose(&self, problem: &mut ConstraintProblem) {
|
||||||
let index = self.column_index().expect(
|
let index = self.column_index().expect(
|
||||||
|
@ -207,8 +223,8 @@ pub struct Point {
|
||||||
pub label: String,
|
pub label: String,
|
||||||
pub color: ElementColor,
|
pub color: ElementColor,
|
||||||
pub representation: Signal<DVector<f64>>,
|
pub representation: Signal<DVector<f64>>,
|
||||||
pub regulators: Signal<BTreeSet<RegulatorKey>>,
|
pub regulators: Signal<Vec<Rc<dyn Regulator>>>,
|
||||||
pub serial: u64,
|
serial: u64,
|
||||||
column_index: Cell<Option<usize>>
|
column_index: Cell<Option<usize>>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -226,7 +242,7 @@ impl Point {
|
||||||
label,
|
label,
|
||||||
color,
|
color,
|
||||||
representation: create_signal(representation),
|
representation: create_signal(representation),
|
||||||
regulators: create_signal(BTreeSet::default()),
|
regulators: create_signal(Vec::new()),
|
||||||
serial: Self::next_serial(),
|
serial: Self::next_serial(),
|
||||||
column_index: None.into()
|
column_index: None.into()
|
||||||
}
|
}
|
||||||
|
@ -259,14 +275,10 @@ impl Element for Point {
|
||||||
self.representation
|
self.representation
|
||||||
}
|
}
|
||||||
|
|
||||||
fn regulators(&self) -> Signal<BTreeSet<RegulatorKey>> {
|
fn regulators(&self) -> Signal<Vec<Rc<dyn Regulator>>> {
|
||||||
self.regulators
|
self.regulators
|
||||||
}
|
}
|
||||||
|
|
||||||
fn serial(&self) -> u64 {
|
|
||||||
self.serial
|
|
||||||
}
|
|
||||||
|
|
||||||
fn column_index(&self) -> Option<usize> {
|
fn column_index(&self) -> Option<usize> {
|
||||||
self.column_index.get()
|
self.column_index.get()
|
||||||
}
|
}
|
||||||
|
@ -276,6 +288,12 @@ impl Element for Point {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Serial for Point {
|
||||||
|
fn serial(&self) -> u64 {
|
||||||
|
self.serial
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ProblemPoser for Point {
|
impl ProblemPoser for Point {
|
||||||
fn pose(&self, problem: &mut ConstraintProblem) {
|
fn pose(&self, problem: &mut ConstraintProblem) {
|
||||||
let index = self.column_index().expect(
|
let index = self.column_index().expect(
|
||||||
|
@ -287,7 +305,7 @@ impl ProblemPoser for Point {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Regulator: ProblemPoser + OutlineItem {
|
pub trait Regulator: Serial + ProblemPoser + OutlineItem {
|
||||||
fn subjects(&self) -> Vec<Rc<dyn Element>>;
|
fn subjects(&self) -> Vec<Rc<dyn Element>>;
|
||||||
fn measurement(&self) -> ReadSignal<f64>;
|
fn measurement(&self) -> ReadSignal<f64>;
|
||||||
fn set_point(&self) -> Signal<SpecifiedValue>;
|
fn set_point(&self) -> Signal<SpecifiedValue>;
|
||||||
|
@ -303,10 +321,25 @@ pub trait Regulator: ProblemPoser + OutlineItem {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Hash for dyn Regulator {
|
||||||
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
|
<dyn Serial>::hash(self, state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for dyn Regulator {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
<dyn Serial>::eq(self, other)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for dyn Regulator {}
|
||||||
|
|
||||||
pub struct InversiveDistanceRegulator {
|
pub struct InversiveDistanceRegulator {
|
||||||
pub subjects: [Rc<dyn Element>; 2],
|
pub subjects: [Rc<dyn Element>; 2],
|
||||||
pub measurement: ReadSignal<f64>,
|
pub measurement: ReadSignal<f64>,
|
||||||
pub set_point: Signal<SpecifiedValue>
|
pub set_point: Signal<SpecifiedValue>,
|
||||||
|
serial: u64
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InversiveDistanceRegulator {
|
impl InversiveDistanceRegulator {
|
||||||
|
@ -321,8 +354,9 @@ impl InversiveDistanceRegulator {
|
||||||
});
|
});
|
||||||
|
|
||||||
let set_point = create_signal(SpecifiedValue::from_empty_spec());
|
let set_point = create_signal(SpecifiedValue::from_empty_spec());
|
||||||
|
let serial = Self::next_serial();
|
||||||
|
|
||||||
InversiveDistanceRegulator { subjects, measurement, set_point }
|
InversiveDistanceRegulator { subjects, measurement, set_point, serial }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -340,6 +374,12 @@ impl Regulator for InversiveDistanceRegulator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Serial for InversiveDistanceRegulator {
|
||||||
|
fn serial(&self) -> u64 {
|
||||||
|
self.serial
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ProblemPoser for InversiveDistanceRegulator {
|
impl ProblemPoser for InversiveDistanceRegulator {
|
||||||
fn pose(&self, problem: &mut ConstraintProblem) {
|
fn pose(&self, problem: &mut ConstraintProblem) {
|
||||||
self.set_point.with_untracked(|set_pt| {
|
self.set_point.with_untracked(|set_pt| {
|
||||||
|
@ -358,7 +398,8 @@ impl ProblemPoser for InversiveDistanceRegulator {
|
||||||
pub struct HalfCurvatureRegulator {
|
pub struct HalfCurvatureRegulator {
|
||||||
pub subject: Rc<dyn Element>,
|
pub subject: Rc<dyn Element>,
|
||||||
pub measurement: ReadSignal<f64>,
|
pub measurement: ReadSignal<f64>,
|
||||||
pub set_point: Signal<SpecifiedValue>
|
pub set_point: Signal<SpecifiedValue>,
|
||||||
|
serial: u64
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HalfCurvatureRegulator {
|
impl HalfCurvatureRegulator {
|
||||||
|
@ -368,8 +409,9 @@ impl HalfCurvatureRegulator {
|
||||||
);
|
);
|
||||||
|
|
||||||
let set_point = create_signal(SpecifiedValue::from_empty_spec());
|
let set_point = create_signal(SpecifiedValue::from_empty_spec());
|
||||||
|
let serial = Self::next_serial();
|
||||||
|
|
||||||
HalfCurvatureRegulator { subject, measurement, set_point }
|
HalfCurvatureRegulator { subject, measurement, set_point, serial }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -399,6 +441,12 @@ impl Regulator for HalfCurvatureRegulator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Serial for HalfCurvatureRegulator {
|
||||||
|
fn serial(&self) -> u64 {
|
||||||
|
self.serial
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ProblemPoser for HalfCurvatureRegulator {
|
impl ProblemPoser for HalfCurvatureRegulator {
|
||||||
fn pose(&self, problem: &mut ConstraintProblem) {
|
fn pose(&self, problem: &mut ConstraintProblem) {
|
||||||
self.set_point.with_untracked(|set_pt| {
|
self.set_point.with_untracked(|set_pt| {
|
||||||
|
@ -502,7 +550,7 @@ impl Assembly {
|
||||||
|
|
||||||
pub fn insert_regulator(&self, regulator: Rc<dyn Regulator>) {
|
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 key = self.regulators.update(
|
self.regulators.update(
|
||||||
|regs| regs.insert(regulator.clone())
|
|regs| regs.insert(regulator.clone())
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -511,7 +559,7 @@ impl Assembly {
|
||||||
|subj| subj.regulators()
|
|subj| subj.regulators()
|
||||||
).collect();
|
).collect();
|
||||||
for regulators in subject_regulators {
|
for regulators in subject_regulators {
|
||||||
regulators.update(|regs| regs.insert(key));
|
regulators.update(|regs| regs.push(regulator.clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// update the realization when the regulator becomes a constraint, or is
|
// update the realization when the regulator becomes a constraint, or is
|
||||||
|
|
|
@ -13,8 +13,7 @@ use crate::{
|
||||||
Element,
|
Element,
|
||||||
HalfCurvatureRegulator,
|
HalfCurvatureRegulator,
|
||||||
InversiveDistanceRegulator,
|
InversiveDistanceRegulator,
|
||||||
Regulator,
|
Regulator
|
||||||
RegulatorKey
|
|
||||||
},
|
},
|
||||||
specified::SpecifiedValue
|
specified::SpecifiedValue
|
||||||
};
|
};
|
||||||
|
@ -90,12 +89,12 @@ fn RegulatorInput(regulator: Rc<dyn Regulator>) -> View {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait OutlineItem {
|
pub trait OutlineItem {
|
||||||
fn outline_item(self: Rc<Self>, element: Rc<dyn Element>) -> View;
|
fn outline_item(self: Rc<Self>, element: &Rc<dyn Element>) -> View;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OutlineItem for InversiveDistanceRegulator {
|
impl OutlineItem for InversiveDistanceRegulator {
|
||||||
fn outline_item(self: Rc<Self>, element: Rc<dyn Element>) -> View {
|
fn outline_item(self: Rc<Self>, element: &Rc<dyn Element>) -> View {
|
||||||
let other_subject_label = if self.subjects[0] == element {
|
let other_subject_label = if self.subjects[0] == element.clone() {
|
||||||
self.subjects[1].label()
|
self.subjects[1].label()
|
||||||
} else {
|
} else {
|
||||||
self.subjects[0].label()
|
self.subjects[0].label()
|
||||||
|
@ -112,7 +111,7 @@ impl OutlineItem for InversiveDistanceRegulator {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OutlineItem for HalfCurvatureRegulator {
|
impl OutlineItem for HalfCurvatureRegulator {
|
||||||
fn outline_item(self: Rc<Self>, _element: Rc<dyn Element>) -> View {
|
fn outline_item(self: Rc<Self>, _element: &Rc<dyn Element>) -> View {
|
||||||
view! {
|
view! {
|
||||||
li(class="regulator") {
|
li(class="regulator") {
|
||||||
div(class="regulator-label") // for spacing
|
div(class="regulator-label") // for spacing
|
||||||
|
@ -124,16 +123,6 @@ impl OutlineItem for HalfCurvatureRegulator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// a list item that shows a regulator in an outline view of an element
|
|
||||||
#[component(inline_props)]
|
|
||||||
fn RegulatorOutlineItem(regulator_key: RegulatorKey, element: Rc<dyn Element>) -> View {
|
|
||||||
let state = use_context::<AppState>();
|
|
||||||
let regulator = state.assembly.regulators.with(
|
|
||||||
|regs| regs[regulator_key].clone()
|
|
||||||
);
|
|
||||||
regulator.outline_item(element)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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(element: Rc<dyn Element>) -> View {
|
fn ElementOutlineItem(element: Rc<dyn Element>) -> View {
|
||||||
|
@ -158,14 +147,10 @@ fn ElementOutlineItem(element: Rc<dyn Element>) -> View {
|
||||||
};
|
};
|
||||||
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
|
|regs| regs
|
||||||
.clone()
|
.clone()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.sorted_by_key(
|
.sorted_by_key(|reg| reg.subjects().len())
|
||||||
|®_key| state.assembly.regulators.with(
|
|
||||||
|regs| regs[reg_key].subjects().len()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.collect()
|
.collect()
|
||||||
);
|
);
|
||||||
let details_node = create_node_ref();
|
let details_node = create_node_ref();
|
||||||
|
@ -223,16 +208,8 @@ fn ElementOutlineItem(element: Rc<dyn Element>) -> View {
|
||||||
ul(class="regulators") {
|
ul(class="regulators") {
|
||||||
Keyed(
|
Keyed(
|
||||||
list=regulator_list,
|
list=regulator_list,
|
||||||
view=move |reg_key| {
|
view=move |reg| reg.outline_item(&element),
|
||||||
let element_for_view = element.clone();
|
key=|reg| reg.serial()
|
||||||
view! {
|
|
||||||
RegulatorOutlineItem(
|
|
||||||
regulator_key=reg_key,
|
|
||||||
element=element_for_view
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
key=|reg_key| reg_key.clone()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue