Integrate engine into application prototype #15
@ -12,7 +12,8 @@ fn load_gen_assemb(assembly: &Assembly) {
|
||||
label: String::from("Castor"),
|
||||
color: [1.00_f32, 0.25_f32, 0.00_f32],
|
||||
rep: engine::sphere(0.5, 0.5, 0.0, 1.0),
|
||||
constraints: BTreeSet::default()
|
||||
constraints: BTreeSet::default(),
|
||||
index: 0
|
||||
}
|
||||
);
|
||||
let _ = assembly.try_insert_element(
|
||||
@ -21,7 +22,8 @@ fn load_gen_assemb(assembly: &Assembly) {
|
||||
label: String::from("Pollux"),
|
||||
color: [0.00_f32, 0.25_f32, 1.00_f32],
|
||||
rep: engine::sphere(-0.5, -0.5, 0.0, 1.0),
|
||||
constraints: BTreeSet::default()
|
||||
constraints: BTreeSet::default(),
|
||||
index: 0
|
||||
}
|
||||
);
|
||||
let _ = assembly.try_insert_element(
|
||||
@ -30,7 +32,8 @@ fn load_gen_assemb(assembly: &Assembly) {
|
||||
label: String::from("Ursa major"),
|
||||
color: [0.25_f32, 0.00_f32, 1.00_f32],
|
||||
rep: engine::sphere(-0.5, 0.5, 0.0, 0.75),
|
||||
constraints: BTreeSet::default()
|
||||
constraints: BTreeSet::default(),
|
||||
index: 0
|
||||
}
|
||||
);
|
||||
let _ = assembly.try_insert_element(
|
||||
@ -39,7 +42,8 @@ fn load_gen_assemb(assembly: &Assembly) {
|
||||
label: String::from("Ursa minor"),
|
||||
color: [0.25_f32, 1.00_f32, 0.00_f32],
|
||||
rep: engine::sphere(0.5, -0.5, 0.0, 0.5),
|
||||
constraints: BTreeSet::default()
|
||||
constraints: BTreeSet::default(),
|
||||
index: 0
|
||||
}
|
||||
);
|
||||
let _ = assembly.try_insert_element(
|
||||
@ -48,7 +52,8 @@ fn load_gen_assemb(assembly: &Assembly) {
|
||||
label: String::from("Deimos"),
|
||||
color: [0.75_f32, 0.75_f32, 0.00_f32],
|
||||
rep: engine::sphere(0.0, 0.15, 1.0, 0.25),
|
||||
constraints: BTreeSet::default()
|
||||
constraints: BTreeSet::default(),
|
||||
index: 0
|
||||
}
|
||||
);
|
||||
let _ = assembly.try_insert_element(
|
||||
@ -57,17 +62,8 @@ fn load_gen_assemb(assembly: &Assembly) {
|
||||
label: String::from("Phobos"),
|
||||
color: [0.00_f32, 0.75_f32, 0.50_f32],
|
||||
rep: engine::sphere(0.0, -0.15, -1.0, 0.25),
|
||||
constraints: BTreeSet::default()
|
||||
}
|
||||
);
|
||||
assembly.insert_constraint(
|
||||
Constraint {
|
||||
args: (
|
||||
assembly.elements_by_id.with_untracked(|elts_by_id| elts_by_id["gemini_a"]),
|
||||
assembly.elements_by_id.with_untracked(|elts_by_id| elts_by_id["gemini_b"])
|
||||
),
|
||||
rep: 0.5,
|
||||
active: create_signal(true)
|
||||
constraints: BTreeSet::default(),
|
||||
index: 0
|
||||
}
|
||||
);
|
||||
}
|
||||
@ -81,7 +77,8 @@ fn load_low_curv_assemb(assembly: &Assembly) {
|
||||
label: "Central".to_string(),
|
||||
color: [0.75_f32, 0.75_f32, 0.75_f32],
|
||||
rep: engine::sphere(0.0, 0.0, 0.0, 1.0),
|
||||
constraints: BTreeSet::default()
|
||||
constraints: BTreeSet::default(),
|
||||
index: 0
|
||||
}
|
||||
);
|
||||
let _ = assembly.try_insert_element(
|
||||
@ -90,7 +87,8 @@ fn load_low_curv_assemb(assembly: &Assembly) {
|
||||
label: "Assembly plane".to_string(),
|
||||
color: [0.75_f32, 0.75_f32, 0.75_f32],
|
||||
rep: engine::sphere_with_offset(0.0, 0.0, 1.0, 0.0, 0.0),
|
||||
constraints: BTreeSet::default()
|
||||
constraints: BTreeSet::default(),
|
||||
index: 0
|
||||
}
|
||||
);
|
||||
let _ = assembly.try_insert_element(
|
||||
@ -99,7 +97,8 @@ fn load_low_curv_assemb(assembly: &Assembly) {
|
||||
label: "Side 1".to_string(),
|
||||
color: [1.00_f32, 0.00_f32, 0.25_f32],
|
||||
rep: engine::sphere_with_offset(1.0, 0.0, 0.0, 1.0, 0.0),
|
||||
constraints: BTreeSet::default()
|
||||
constraints: BTreeSet::default(),
|
||||
index: 0
|
||||
}
|
||||
);
|
||||
let _ = assembly.try_insert_element(
|
||||
@ -108,7 +107,8 @@ fn load_low_curv_assemb(assembly: &Assembly) {
|
||||
label: "Side 2".to_string(),
|
||||
color: [0.25_f32, 1.00_f32, 0.00_f32],
|
||||
rep: engine::sphere_with_offset(-0.5, a, 0.0, 1.0, 0.0),
|
||||
constraints: BTreeSet::default()
|
||||
constraints: BTreeSet::default(),
|
||||
index: 0
|
||||
}
|
||||
);
|
||||
let _ = assembly.try_insert_element(
|
||||
@ -117,7 +117,8 @@ fn load_low_curv_assemb(assembly: &Assembly) {
|
||||
label: "Side 3".to_string(),
|
||||
color: [0.00_f32, 0.25_f32, 1.00_f32],
|
||||
rep: engine::sphere_with_offset(-0.5, -a, 0.0, 1.0, 0.0),
|
||||
constraints: BTreeSet::default()
|
||||
constraints: BTreeSet::default(),
|
||||
index: 0
|
||||
}
|
||||
);
|
||||
let _ = assembly.try_insert_element(
|
||||
@ -126,7 +127,8 @@ fn load_low_curv_assemb(assembly: &Assembly) {
|
||||
label: "Corner 1".to_string(),
|
||||
color: [0.75_f32, 0.75_f32, 0.75_f32],
|
||||
rep: engine::sphere(-4.0/3.0, 0.0, 0.0, 1.0/3.0),
|
||||
constraints: BTreeSet::default()
|
||||
constraints: BTreeSet::default(),
|
||||
index: 0
|
||||
}
|
||||
);
|
||||
let _ = assembly.try_insert_element(
|
||||
@ -135,7 +137,8 @@ fn load_low_curv_assemb(assembly: &Assembly) {
|
||||
label: "Corner 2".to_string(),
|
||||
color: [0.75_f32, 0.75_f32, 0.75_f32],
|
||||
rep: engine::sphere(2.0/3.0, -4.0/3.0 * a, 0.0, 1.0/3.0),
|
||||
constraints: BTreeSet::default()
|
||||
constraints: BTreeSet::default(),
|
||||
index: 0
|
||||
}
|
||||
);
|
||||
let _ = assembly.try_insert_element(
|
||||
@ -144,7 +147,8 @@ fn load_low_curv_assemb(assembly: &Assembly) {
|
||||
label: String::from("Corner 3"),
|
||||
color: [0.75_f32, 0.75_f32, 0.75_f32],
|
||||
rep: engine::sphere(2.0/3.0, 4.0/3.0 * a, 0.0, 1.0/3.0),
|
||||
constraints: BTreeSet::default()
|
||||
constraints: BTreeSet::default(),
|
||||
index: 0
|
||||
}
|
||||
);
|
||||
}
|
||||
@ -215,6 +219,7 @@ pub fn AddRemove() -> View {
|
||||
rep: 0.0,
|
||||
active: create_signal(true)
|
||||
});
|
||||
state.assembly.realize();
|
||||
state.selection.update(|sel| sel.clear());
|
||||
|
||||
/* DEBUG */
|
||||
|
@ -1,8 +1,11 @@
|
||||
use nalgebra::DVector;
|
||||
use nalgebra::{DMatrix, DVector};
|
||||
use rustc_hash::FxHashMap;
|
||||
use slab::Slab;
|
||||
use std::collections::BTreeSet;
|
||||
use sycamore::prelude::*;
|
||||
use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */
|
||||
|
||||
use crate::engine::{realize_gram, PartialMatrix};
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct Element {
|
||||
@ -10,7 +13,10 @@ pub struct Element {
|
||||
pub label: String,
|
||||
pub color: [f32; 3],
|
||||
glen marked this conversation as resolved
Outdated
|
||||
pub rep: DVector<f64>,
|
||||
glen marked this conversation as resolved
Outdated
glen
commented
Same comments go for rep here as in Constraint. Maybe it's "representative"? If your worry is that a full word will be too long, look for a shorter word, or pick a unique short acronym for this sort of thing in both places, and then clearly document what that acronym means and why you are using it. Maybe for now "document" just means "comment" until we have a decent doc system in place. Same comments go for rep here as in Constraint. Maybe it's "representative"? If your worry is that a full word will be too long, look for a shorter word, or pick a unique short acronym for this sort of thing in both places, and then clearly document what that acronym means and why you are using it. Maybe for now "document" just means "comment" until we have a decent doc system in place.
Vectornaut
commented
It's for "representation"—a vector representation of the element, acted on by a linear representation of the 3d Möbius group. When I update the PR, I'll expand the property name to > Same comments go for rep here as in Constraint. Maybe it's "representative"?
It's for "representation"—a vector representation of the element, acted on by a linear representation of the 3d Möbius group. When I update the PR, I'll expand the property name to `representation` (in commit da008fd).
glen
commented
One other comment: some of the stuff in the assembly is intrinsic, i.e. there is a red sphere, constrained to have radius 1, containing a point with id A labeled One other comment: some of the stuff in the assembly is intrinsic, i.e. there _is_ a red sphere, constrained to have radius 1, containing a point with id A labeled `A_{init}`, say. Other stuff is contingent: the current realization of the red sphere has such-and-such representation, and point id A is selected. (I think the selection needs to be constant and shared across all views, so may as well be in the Assembly.) There may turn out to be practical reasons to separate those types of things in some way, but I certainly think there are conceptual advantages to doing so. For now, we may just want to keep in mind to choose names that suggest the intrinsic/contingent divide. It may be that "representation" is sufficiently suggestive in this way, or perhaps a name like "realization" would be better, and "currentRealization" is even stronger in this regard. Similarly, "selection" might already feel contingent enough; "currentlySelected" certainly would. This is not a request for any particular change to the current PR, just an item to keep in mind, although if you are moved to make any changes that's OK too.
|
||||
pub constraints: BTreeSet<usize>
|
||||
pub constraints: BTreeSet<usize>,
|
||||
|
||||
// internal properties, not reflected in any view
|
||||
pub index: usize
|
||||
glen marked this conversation as resolved
Outdated
glen
commented
I am confused; if this is internal, why is it Also, I am guessing that this "index" is the same value that will appear for example in the pair in a Constraint that identifies the elements that are being constrained? If so, then the same typedef should be used here, and the names should be harmonized: if you land on And then to soften my first paragraph: maybe we are encouraging views to use these handles for efficiency? Then we should not describe them as "internal" but instead document that they are not intrinsic properties of an element and not to be relied on across a reload of an assembly, but can be used in XXX circumstances and will be invariant over YYY operations, that sort of thing. Oh, and then I see that Assembly has an Note I have been typing camelCase and your code is using snake_case. The former is more compact, and I am quite used to it. But I am not stuck on it. We should discuss and pick a casing convention that suits us both. I give zero weight to Rust's casing conventions; as I said, the rigidity of that community does not seem helpful, and we will be writing in husht anyway. I am confused; if this is internal, why is it `pub`? If it is important that this property never be reflected in a view (i.e., it is not really an intrinsic property of the element, just a contingent value for programming convenience) then there should be a way to enforce that views don't see it. Does Rust have anything like C++ `friend`s or `protected` or something? Ideally, anything that shouldn't be accessible in a view would not be exposed in an interface to the assembly that views can get a hold of.
Also, I am guessing that this "index" is the same value that will appear for example in the pair in a Constraint that identifies the elements that are being constrained? If so, then the same typedef should be used here, and the names should be harmonized: if you land on `ElementHandle`, say, then this should be `handle`.
And then to soften my first paragraph: maybe we are encouraging views to use these handles for efficiency? Then we should not describe them as "internal" but instead document that they are not intrinsic properties of an element and not to be relied on across a reload of an assembly, but can be used in XXX circumstances and will be invariant over YYY operations, that sort of thing.
Oh, and then I see that Assembly has an `elements_by_id` property. Once you've settled on names for the relevant concept, say it's an `ElementHandle`, that should be renamed to something like `handleOfId` or `elementHandleOfId`.
Note I have been typing camelCase and your code is using snake_case. The former is more compact, and I am quite used to it. But I am not stuck on it. We should discuss and pick a casing convention that suits us both. I give **zero** weight to Rust's casing conventions; as I said, the rigidity of that community does not seem helpful, and we will be writing in husht anyway.
Vectornaut
commented
When an assembly with n elements is sent to the engine, each element gets an index from 1 to n, which tells you which column of the configuration matrix it goes in. The index is assigned and used during the realization routine,
Good point—it shouldn't be. I made it public in this pull request as a kludge, because I hadn't written a constructor for When an assembly with *n* elements is sent to the engine, each element gets an index from 1 to *n*, which tells you which column of the configuration matrix it goes in. The index is assigned and used during the realization routine, `Assembly::realize`; it can change each time `realize` is called. I've put it in the `Element` structure, as the `index` field, because that feels like the most foolproof way to ensure consistent indexing of the configuration matrix and the Gram matrix.
> if this is internal, why is it `pub`?
Good point—it shouldn't be. I made it public in this pull request as a kludge, because I hadn't written a constructor for `Element` yet. The outline cleanup pull request (#16) introduces an `Element` constructor, allowing us to make `index` private with no changes to any other code. I propose making this an issue and then addressing it in #16.
glen
commented
Fine, given that #16 is around the corner. Fine, given that #16 is around the corner.
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@ -40,6 +46,8 @@ impl Assembly {
|
||||
}
|
||||
}
|
||||
|
||||
// --- inserting elements and constraints ---
|
||||
|
||||
// insert an element into the assembly without checking whether we already
|
||||
// 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
|
||||
@ -77,7 +85,8 @@ impl Assembly {
|
||||
label: format!("Sphere {}", id_num),
|
||||
color: [0.75_f32, 0.75_f32, 0.75_f32],
|
||||
rep: DVector::<f64>::from_column_slice(&[0.0, 0.0, 0.0, 0.5, -0.5]),
|
||||
constraints: BTreeSet::default()
|
||||
constraints: BTreeSet::default(),
|
||||
index: 0
|
||||
}
|
||||
);
|
||||
glen marked this conversation as resolved
Outdated
glen
commented
I am very confused by I am very confused by `index: 0`. I guess I was wrong that the index is a quick way to get at the element within the assembly? Maybe this will all be clear once the naming of types and properties is a bit improved, but if not, please comment this line.
Vectornaut
commented
The PR update will add a comment that explains the The PR update will add a comment that explains the `index` field's meaning, as described in [this comment](https://code.studioinfinity.org/glen/dyna3/pulls/15#issuecomment-1816), and reminds us to make `index` private when that becomes possible (for example, in pull request #16).
glen
commented
Great. And please add to #16 to rename this property to something that has "column" in it -- that's much more informative than index, which could be an index into anything. Great. And please add to #16 to rename this property to something that has "column" in it -- that's much more informative than index, which could be an index into anything.
|
||||
}
|
||||
@ -90,4 +99,83 @@ impl Assembly {
|
||||
elts[args.1].constraints.insert(key);
|
||||
})
|
||||
}
|
||||
|
||||
// --- realization ---
|
||||
|
||||
pub fn realize(&self) {
|
||||
// index the elements
|
||||
self.elements.update_silent(|elts| {
|
||||
for (index, (_, elt)) in elts.into_iter().enumerate() {
|
||||
elt.index = index;
|
||||
}
|
||||
});
|
||||
|
||||
// set up the Gram matrix and the initial configuration matrix
|
||||
let (gram, guess) = self.elements.with_untracked(|elts| {
|
||||
// set up the off-diagonal part of the Gram matrix
|
||||
let mut gram_to_be = PartialMatrix::new();
|
||||
self.constraints.with_untracked(|csts| {
|
||||
for (_, cst) in csts {
|
||||
let args = cst.args;
|
||||
let row = elts[args.0].index;
|
||||
let col = elts[args.1].index;
|
||||
gram_to_be.push_sym(row, col, cst.rep);
|
||||
}
|
||||
});
|
||||
|
||||
// set up the initial configuration matrix and the diagonal of the
|
||||
// Gram matrix
|
||||
let mut guess_to_be = DMatrix::<f64>::zeros(5, elts.len());
|
||||
for (_, elt) in elts {
|
||||
let index = elt.index;
|
||||
gram_to_be.push_sym(index, index, 1.0);
|
||||
guess_to_be.set_column(index, &elt.rep);
|
||||
}
|
||||
|
||||
(gram_to_be, guess_to_be)
|
||||
});
|
||||
|
||||
/* DEBUG */
|
||||
// log the Gram matrix
|
||||
console::log_1(&JsValue::from("Gram matrix:"));
|
||||
gram.log_to_console();
|
||||
|
||||
/* DEBUG */
|
||||
// log the initial configuration matrix
|
||||
console::log_1(&JsValue::from("old configuration:"));
|
||||
for j in 0..guess.nrows() {
|
||||
let mut row_str = String::new();
|
||||
for k in 0..guess.ncols() {
|
||||
row_str.push_str(format!(" {:>8.3}", guess[(j, k)]).as_str());
|
||||
glen marked this conversation as resolved
Outdated
glen
commented
Do you want to set up now a logging/debugging system so that we can leave messages like this in/out at compile time (so that they have no runtime impact when left out), and so always leave them in the code in the repo? Or do you think the project is premature for that, we will plan to take these out in the fairly near future? Either way is fine, just let me know; I am just making the point that we definitely don't want any raw console logs in our standard code, ultimately. Do you want to set up now a logging/debugging system so that we can leave messages like this in/out at compile time (so that they have no runtime impact when left out), and so always leave them in the code in the repo? Or do you think the project is premature for that, we will plan to take these out in the fairly near future? Either way is fine, just let me know; I am just making the point that we definitely don't want any raw console logs in our standard code, ultimately.
Vectornaut
commented
I think it's too early to design a formal logging system. I've been adding and removing log messages pretty freely as I test new features and track down bugs. I just started a wiki page where we can jot down log messages that come in handy; we can refer to that list when we start thinking about how to log stuff more formally. I think it's too early to design a formal logging system. I've been adding and removing log messages pretty freely as I test new features and track down bugs. I just started a [wiki page](wiki/Logging) where we can jot down log messages that come in handy; we can refer to that list when we start thinking about how to log stuff more formally.
|
||||
}
|
||||
console::log_1(&JsValue::from(row_str));
|
||||
}
|
||||
|
||||
// look for a configuration with the given Gram matrix
|
||||
let (config, success, history) = realize_gram(
|
||||
&gram, guess, &[],
|
||||
1.0e-12, 0.5, 0.9, 1.1, 200, 110
|
||||
);
|
||||
|
||||
/* DEBUG */
|
||||
// report the outcome of the search
|
||||
console::log_1(&JsValue::from(
|
||||
if success {
|
||||
"Target accuracy achieved!"
|
||||
} else {
|
||||
"Failed to reach target accuracy"
|
||||
}
|
||||
));
|
||||
console::log_2(&JsValue::from("Steps:"), &JsValue::from(history.scaled_loss.len() - 1));
|
||||
console::log_2(&JsValue::from("Loss:"), &JsValue::from(*history.scaled_loss.last().unwrap()));
|
||||
|
||||
if success {
|
||||
// read out the solution
|
||||
self.elements.update(|elts| {
|
||||
glen marked this conversation as resolved
glen
commented
Are the _1 and _2 suffixes the number of arguments? I take it rust doesn't have functions with a varying number of arguments? Or do they mean something else? Are the _1 and _2 suffixes the number of arguments? I take it rust doesn't have functions with a varying number of arguments? Or do they mean something else?
Vectornaut
commented
Yes— > Are the _1 and _2 suffixes the number of arguments?
Yes—[`log_0`](https://docs.rs/web-sys/latest/web_sys/console/fn.log_0.html) through [`log_7`](https://docs.rs/web-sys/latest/web_sys/console/fn.log_7.html) are convenience wrappers for [`log`](https://docs.rs/web-sys/latest/web_sys/console/fn.log.html), which takes an array of things to log.
glen
commented
Is that a Rustism? Or a local convenience? It's pretty awful; I hope we can avoid it in the long run, one way or another. Is that a Rustism? Or a local convenience? It's pretty awful; I hope we can avoid it in the long run, one way or another.
|
||||
for (_, elt) in elts.iter_mut() {
|
||||
elt.rep.set_column(0, &config.column(elt.index));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
use lazy_static::lazy_static;
|
||||
use nalgebra::{Const, DMatrix, DVector, Dyn};
|
||||
use web_sys::{console, wasm_bindgen::JsValue}; /* DEBUG */
|
||||
|
||||
// --- elements ---
|
||||
|
||||
@ -40,9 +41,30 @@ struct MatrixEntry {
|
||||
value: f64
|
||||
}
|
||||
|
||||
struct PartialMatrix(Vec<MatrixEntry>);
|
||||
pub struct PartialMatrix(Vec<MatrixEntry>);
|
||||
|
||||
impl PartialMatrix {
|
||||
pub fn new() -> PartialMatrix {
|
||||
PartialMatrix(Vec::<MatrixEntry>::new())
|
||||
}
|
||||
|
||||
pub fn push_sym(&mut self, row: usize, col: usize, value: f64) {
|
||||
let PartialMatrix(entries) = self;
|
||||
entries.push(MatrixEntry { index: (row, col), value: value });
|
||||
if row != col {
|
||||
entries.push(MatrixEntry { index: (col, row), value: value });
|
||||
}
|
||||
}
|
||||
|
||||
/* DEBUG */
|
||||
pub fn log_to_console(&self) {
|
||||
let PartialMatrix(entries) = self;
|
||||
for ent in entries {
|
||||
let ent_str = format!("{} {} {}", ent.index.0, ent.index.1, ent.value);
|
||||
console::log_1(&JsValue::from(ent_str.as_str()));
|
||||
}
|
||||
}
|
||||
|
||||
fn proj(&self, a: &DMatrix<f64>) -> DMatrix<f64> {
|
||||
let mut result = DMatrix::<f64>::zeros(a.nrows(), a.ncols());
|
||||
let PartialMatrix(entries) = self;
|
||||
@ -64,13 +86,13 @@ impl PartialMatrix {
|
||||
|
||||
// --- descent history ---
|
||||
|
||||
struct DescentHistory {
|
||||
config: Vec<DMatrix<f64>>,
|
||||
scaled_loss: Vec<f64>,
|
||||
neg_grad: Vec<DMatrix<f64>>,
|
||||
min_eigval: Vec<f64>,
|
||||
base_step: Vec<DMatrix<f64>>,
|
||||
backoff_steps: Vec<i32>
|
||||
pub struct DescentHistory {
|
||||
pub config: Vec<DMatrix<f64>>,
|
||||
pub scaled_loss: Vec<f64>,
|
||||
pub neg_grad: Vec<DMatrix<f64>>,
|
||||
pub min_eigval: Vec<f64>,
|
||||
pub base_step: Vec<DMatrix<f64>>,
|
||||
pub backoff_steps: Vec<i32>
|
||||
}
|
||||
|
||||
impl DescentHistory {
|
||||
@ -148,7 +170,7 @@ fn seek_better_config(
|
||||
|
||||
// seek a matrix `config` for which `config' * Q * config` matches the partial
|
||||
// matrix `gram`. use gradient descent starting from `guess`
|
||||
fn realize_gram(
|
||||
pub fn realize_gram(
|
||||
gram: &PartialMatrix,
|
||||
guess: DMatrix<f64>,
|
||||
frozen: &[(usize, usize)],
|
||||
|
Shouldn't we have at least a typedef somewhere for color representations, in case we use a different color space at some point? For example, we are almost certain to want to add an opacity level to our colors. (Certainly the final product will allow you to set the opacity of any element.)
Done, with the type alias
ElementColor
.OK, we will use that for now; the name implies we might have a different type for the color of something else, like the axes in a view (which aren't part of the Assembly). If we do start using this type for the colors of other things, we should rename it, I think.