2024-10-21 23:38:27 +00:00
|
|
|
use core::array;
|
Manipulate the assembly (#29)
feat: Find tangent space of solution variety, use for perturbations
### Tangent space
#### Implementation
The structure `engine::ConfigSubspace` represents a subspace of the configuration vector space $\operatorname{Hom}(\mathbb{R}^n, \mathbb{R}^5)$. It holds a basis for the subspace which is orthonormal with respect to the Euclidean inner product. The method `ConfigSubspace::symmetric_kernel` takes an endomorphism of the configuration vector space, which must be symmetric with respect to the Euclidean inner product, and returns its approximate kernel in the form of a `ConfigSubspace`.
At the end of `engine::realize_gram`, we use the computed Hessian to find the tangent space of the solution variety, and we return it alongside the realization. Since altering the constraints can change the tangent space without changing the solution, we compute the tangent space even when the guess passed to the realization routine is already a solution.
After `Assembly::realize` calls `engine::realize_gram`, it saves the returned tangent space in the assembly's `tangent` signal. The basis vectors are stored in configuration matrix format, ordered according to the elements' column indices. To help maintain consistency between the storage layout of the tangent space and the elements' column indices, we switch the column index data type from `usize` to `Option<usize>` and enforce the following invariants:
1. If an element has a column index, its tangent motions can be found in that column of the tangent space basis matrices.
2. If an element is affected by a constraint, it has a column index.
The comments in `assembly.rs` state the invariants and describe how they're enforced.
#### Automated testing
The test `engine::tests::tangent_test` builds a simple assembly with a known tangent space, runs the realization routine, and checks the returned tangent space against a hand-computed basis.
#### Limitations
The method `ConfigSubspace::symmetric_kernel` approximates the kernel by taking all the eigenspaces whose eigenvalues are smaller than a hard-coded threshold size. We may need a more flexible system eventually.
### Deformation
#### Implementation
The main purpose of this implementation is to confirm that deformation works as we'd hoped. The code is messy, and the deformation routine has at least one numerical quirk.
For simplicity, the keyboard commands that manipulate the assembly are handled by the display, just like the keyboard commands that control the camera. Deformation happens at the beginning of the animation loop.
The function `Assembly::deform` works like this:
1. Take a list of element motions
2. Project them onto the tangent space of the solution variety
3. Sum them to get a deformation $v$ of the whole assembly
4. Step the assembly along the "mass shell" geodesic tangent to $v$
* This step stays on the solution variety to first order
5. Call `realize` to bring the assembly back onto the solution variety
#### Manual testing
To manipulate the assembly:
1. Select a sphere
2. Make sure the display has focus
3. Hold the following keys:
* **A**/**D** for $x$ translation
* **W**/**S** for $y$ translation
* **shift**+**W**/**S** for $z$ translation
#### Limitations
Because the manipulation commands are handled by the display, you can only manipulate the assembly when the display has focus.
Since our test assemblies only include spheres, we assume in `Assembly::deform` that every element is a sphere.
When the tangent space is zero, `Assembly::deform` does nothing except print "The assembly is rigid" to the console.
During a deformation, the curvature and co-curvature components of a sphere's vector representation can exhibit weird discontinuous "swaps" that don't visibly affect how the sphere is drawn. *[I'll write more about this in an issue.]*
Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/29
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2024-12-30 22:53:07 +00:00
|
|
|
use nalgebra::{DMatrix, DVector, Rotation3, Vector3};
|
2025-05-06 19:17:30 +00:00
|
|
|
use std::rc::Rc;
|
2024-10-21 23:38:27 +00:00
|
|
|
use sycamore::{prelude::*, motion::create_raf};
|
|
|
|
use web_sys::{
|
|
|
|
console,
|
|
|
|
window,
|
|
|
|
KeyboardEvent,
|
2024-11-27 05:02:06 +00:00
|
|
|
MouseEvent,
|
2024-10-21 23:38:27 +00:00
|
|
|
WebGl2RenderingContext,
|
2025-05-01 19:25:13 +00:00
|
|
|
WebGlBuffer,
|
2024-10-21 23:38:27 +00:00
|
|
|
WebGlProgram,
|
|
|
|
WebGlShader,
|
|
|
|
WebGlUniformLocation,
|
|
|
|
wasm_bindgen::{JsCast, JsValue}
|
|
|
|
};
|
|
|
|
|
2025-05-01 19:25:13 +00:00
|
|
|
use crate::{
|
|
|
|
AppState,
|
2025-05-06 19:17:30 +00:00
|
|
|
assembly::{Element, ElementColor, ElementMotion, Point, Sphere}
|
2025-05-01 19:25:13 +00:00
|
|
|
};
|
|
|
|
|
2025-06-02 15:56:06 +00:00
|
|
|
// --- color ---
|
|
|
|
|
|
|
|
const COLOR_SIZE: usize = 3;
|
|
|
|
type ColorWithOpacity = [f32; COLOR_SIZE + 1];
|
|
|
|
|
|
|
|
fn combine_channels(color: ElementColor, opacity: f32) -> ColorWithOpacity {
|
|
|
|
let mut color_with_opacity = [0.0; COLOR_SIZE + 1];
|
|
|
|
color_with_opacity[..COLOR_SIZE].copy_from_slice(&color);
|
|
|
|
color_with_opacity[COLOR_SIZE] = opacity;
|
|
|
|
color_with_opacity
|
|
|
|
}
|
|
|
|
|
2025-05-01 19:25:13 +00:00
|
|
|
// --- scene data ---
|
|
|
|
|
|
|
|
struct SceneSpheres {
|
|
|
|
representations: Vec<DVector<f64>>,
|
2025-06-02 15:56:06 +00:00
|
|
|
colors_with_opacity: Vec<ColorWithOpacity>,
|
2025-05-01 19:25:13 +00:00
|
|
|
highlights: Vec<f32>
|
|
|
|
}
|
|
|
|
|
|
|
|
impl SceneSpheres {
|
|
|
|
fn new() -> SceneSpheres{
|
|
|
|
SceneSpheres {
|
|
|
|
representations: Vec::new(),
|
2025-06-02 15:56:06 +00:00
|
|
|
colors_with_opacity: Vec::new(),
|
2025-05-01 19:25:13 +00:00
|
|
|
highlights: Vec::new()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn len_i32(&self) -> i32 {
|
|
|
|
self.representations.len().try_into().expect("Number of spheres must fit in a 32-bit integer")
|
|
|
|
}
|
|
|
|
|
2025-06-02 15:56:06 +00:00
|
|
|
fn push(&mut self, representation: DVector<f64>, color: ElementColor, opacity: f32, highlight: f32) {
|
2025-05-01 19:25:13 +00:00
|
|
|
self.representations.push(representation);
|
2025-06-02 15:56:06 +00:00
|
|
|
self.colors_with_opacity.push(combine_channels(color, opacity));
|
2025-05-01 19:25:13 +00:00
|
|
|
self.highlights.push(highlight);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct ScenePoints {
|
|
|
|
representations: Vec<DVector<f64>>,
|
2025-06-02 15:56:06 +00:00
|
|
|
colors_with_opacity: Vec<ColorWithOpacity>,
|
2025-05-01 19:25:13 +00:00
|
|
|
highlights: Vec<f32>,
|
|
|
|
selections: Vec<f32>
|
|
|
|
}
|
|
|
|
|
|
|
|
impl ScenePoints {
|
|
|
|
fn new() -> ScenePoints {
|
|
|
|
ScenePoints {
|
|
|
|
representations: Vec::new(),
|
2025-06-02 15:56:06 +00:00
|
|
|
colors_with_opacity: Vec::new(),
|
2025-05-01 19:25:13 +00:00
|
|
|
highlights: Vec::new(),
|
|
|
|
selections: Vec::new()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-06-02 15:56:06 +00:00
|
|
|
fn push(&mut self, representation: DVector<f64>, color: ElementColor, opacity: f32, highlight: f32, selected: bool) {
|
2025-05-01 19:25:13 +00:00
|
|
|
self.representations.push(representation);
|
2025-06-02 15:56:06 +00:00
|
|
|
self.colors_with_opacity.push(combine_channels(color, opacity));
|
2025-05-01 19:25:13 +00:00
|
|
|
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) {
|
2025-06-02 15:56:06 +00:00
|
|
|
/* SCAFFOLDING */
|
|
|
|
const DEFAULT_OPACITY: f32 = 0.5;
|
|
|
|
const GHOST_OPACITY: f32 = 0.2;
|
|
|
|
const HIGHLIGHT: f32 = 0.2;
|
|
|
|
|
2025-05-01 19:25:13 +00:00
|
|
|
let representation = self.representation.get_clone_untracked();
|
|
|
|
let color = if selected { self.color.map(|channel| 0.2 + 0.8*channel) } else { self.color };
|
2025-06-02 15:56:06 +00:00
|
|
|
let opacity = if self.ghost.get() { GHOST_OPACITY } else { DEFAULT_OPACITY };
|
2025-05-01 19:25:13 +00:00
|
|
|
let highlight = if selected { 1.0 } else { HIGHLIGHT };
|
2025-06-02 15:56:06 +00:00
|
|
|
scene.spheres.push(representation, color, opacity, highlight);
|
2025-05-01 19:25:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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) {
|
2025-06-02 15:56:06 +00:00
|
|
|
/* SCAFFOLDING */
|
|
|
|
const GHOST_OPACITY: f32 = 0.4;
|
|
|
|
const HIGHLIGHT: f32 = 0.5;
|
|
|
|
|
2025-05-01 19:25:13 +00:00
|
|
|
let representation = self.representation.get_clone_untracked();
|
|
|
|
let color = if selected { self.color.map(|channel| 0.2 + 0.8*channel) } else { self.color };
|
2025-06-02 15:56:06 +00:00
|
|
|
let opacity = if self.ghost.get() { GHOST_OPACITY } else { 1.0 };
|
2025-05-01 19:25:13 +00:00
|
|
|
let highlight = if selected { 1.0 } else { HIGHLIGHT };
|
2025-06-02 15:56:06 +00:00
|
|
|
scene.points.push(representation, color, opacity, highlight, selected);
|
2025-05-01 19:25:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/* 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 ---
|
2024-10-21 23:38:27 +00:00
|
|
|
|
|
|
|
fn compile_shader(
|
|
|
|
context: &WebGl2RenderingContext,
|
|
|
|
shader_type: u32,
|
|
|
|
source: &str,
|
|
|
|
) -> WebGlShader {
|
|
|
|
let shader = context.create_shader(shader_type).unwrap();
|
|
|
|
context.shader_source(&shader, source);
|
|
|
|
context.compile_shader(&shader);
|
|
|
|
shader
|
|
|
|
}
|
|
|
|
|
2025-05-01 19:25:13 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2024-10-21 23:38:27 +00:00
|
|
|
fn get_uniform_array_locations<const N: usize>(
|
|
|
|
context: &WebGl2RenderingContext,
|
|
|
|
program: &WebGlProgram,
|
|
|
|
var_name: &str,
|
|
|
|
member_name_opt: Option<&str>
|
|
|
|
) -> [Option<WebGlUniformLocation>; N] {
|
|
|
|
array::from_fn(|n| {
|
|
|
|
let name = match member_name_opt {
|
|
|
|
Some(member_name) => format!("{var_name}[{n}].{member_name}"),
|
|
|
|
None => format!("{var_name}[{n}]")
|
|
|
|
};
|
|
|
|
context.get_uniform_location(&program, name.as_str())
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2025-05-01 19:25:13 +00:00
|
|
|
// bind the given vertex buffer object to the given vertex attribute
|
|
|
|
fn bind_to_attribute(
|
2024-10-21 23:38:27 +00:00
|
|
|
context: &WebGl2RenderingContext,
|
2025-05-01 19:25:13 +00:00
|
|
|
attr_index: u32,
|
|
|
|
attr_size: i32,
|
|
|
|
buffer: &Option<WebGlBuffer>
|
2024-10-21 23:38:27 +00:00
|
|
|
) {
|
2025-05-01 19:25:13 +00:00
|
|
|
context.bind_buffer(WebGl2RenderingContext::ARRAY_BUFFER, buffer.as_ref());
|
|
|
|
context.vertex_attrib_pointer_with_i32(
|
|
|
|
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());
|
2024-10-21 23:38:27 +00:00
|
|
|
|
2025-05-01 19:25:13 +00:00
|
|
|
// load the given data into the buffer. this block is unsafe because
|
|
|
|
// `Float32Array::view` creates a raw view into our module's
|
|
|
|
// `WebAssembly.Memory` buffer. allocating more memory will change the
|
|
|
|
// buffer, invalidating the view, so we have to make sure we don't allocate
|
|
|
|
// any memory until the view is dropped. we're okay here because the view is
|
|
|
|
// used as soon as it's created
|
2024-10-21 23:38:27 +00:00
|
|
|
unsafe {
|
|
|
|
context.buffer_data_with_array_buffer_view(
|
|
|
|
WebGl2RenderingContext::ARRAY_BUFFER,
|
|
|
|
&js_sys::Float32Array::view(&data),
|
|
|
|
WebGl2RenderingContext::STATIC_DRAW,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2025-05-01 19:25:13 +00:00
|
|
|
buffer
|
|
|
|
}
|
|
|
|
|
|
|
|
fn bind_new_buffer_to_attribute(
|
|
|
|
context: &WebGl2RenderingContext,
|
|
|
|
attr_index: u32,
|
|
|
|
attr_size: i32,
|
|
|
|
data: &[f32]
|
|
|
|
) {
|
|
|
|
let buffer = load_new_buffer(context, data);
|
|
|
|
bind_to_attribute(context, attr_index, attr_size, &buffer);
|
2024-10-21 23:38:27 +00:00
|
|
|
}
|
|
|
|
|
2024-11-27 05:02:06 +00:00
|
|
|
// the direction in camera space that a mouse event is pointing along
|
2025-05-01 19:25:13 +00:00
|
|
|
fn event_dir(event: &MouseEvent) -> (Vector3<f64>, f64) {
|
|
|
|
let target: web_sys::Element = event.target().unwrap().unchecked_into();
|
2024-11-27 05:02:06 +00:00
|
|
|
let rect = target.get_bounding_client_rect();
|
|
|
|
let width = rect.width();
|
|
|
|
let height = rect.height();
|
|
|
|
let shortdim = width.min(height);
|
|
|
|
|
2025-05-01 19:25:13 +00:00
|
|
|
// this constant should be kept synchronized with `spheres.frag` and
|
|
|
|
// `point.vert`
|
2024-11-27 05:02:06 +00:00
|
|
|
const FOCAL_SLOPE: f64 = 0.3;
|
|
|
|
|
2025-05-01 19:25:13 +00:00
|
|
|
(
|
|
|
|
Vector3::new(
|
|
|
|
FOCAL_SLOPE * (2.0*(f64::from(event.client_x()) - rect.left()) - width) / shortdim,
|
|
|
|
FOCAL_SLOPE * (2.0*(rect.bottom() - f64::from(event.client_y())) - height) / shortdim,
|
|
|
|
-1.0
|
|
|
|
),
|
|
|
|
FOCAL_SLOPE * 2.0 / shortdim
|
2024-11-27 05:02:06 +00:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2025-05-01 19:25:13 +00:00
|
|
|
// --- display component ---
|
|
|
|
|
2024-10-21 23:38:27 +00:00
|
|
|
#[component]
|
|
|
|
pub fn Display() -> View {
|
|
|
|
let state = use_context::<AppState>();
|
|
|
|
|
|
|
|
// canvas
|
|
|
|
let display = create_node_ref();
|
|
|
|
|
2024-11-27 05:02:06 +00:00
|
|
|
// viewpoint
|
|
|
|
let assembly_to_world = create_signal(DMatrix::<f64>::identity(5, 5));
|
|
|
|
|
2024-10-21 23:38:27 +00:00
|
|
|
// navigation
|
|
|
|
let pitch_up = create_signal(0.0);
|
|
|
|
let pitch_down = create_signal(0.0);
|
|
|
|
let yaw_right = create_signal(0.0);
|
|
|
|
let yaw_left = create_signal(0.0);
|
|
|
|
let roll_ccw = create_signal(0.0);
|
|
|
|
let roll_cw = create_signal(0.0);
|
|
|
|
let zoom_in = create_signal(0.0);
|
|
|
|
let zoom_out = create_signal(0.0);
|
|
|
|
let turntable = create_signal(false); /* BENCHMARKING */
|
|
|
|
|
Manipulate the assembly (#29)
feat: Find tangent space of solution variety, use for perturbations
### Tangent space
#### Implementation
The structure `engine::ConfigSubspace` represents a subspace of the configuration vector space $\operatorname{Hom}(\mathbb{R}^n, \mathbb{R}^5)$. It holds a basis for the subspace which is orthonormal with respect to the Euclidean inner product. The method `ConfigSubspace::symmetric_kernel` takes an endomorphism of the configuration vector space, which must be symmetric with respect to the Euclidean inner product, and returns its approximate kernel in the form of a `ConfigSubspace`.
At the end of `engine::realize_gram`, we use the computed Hessian to find the tangent space of the solution variety, and we return it alongside the realization. Since altering the constraints can change the tangent space without changing the solution, we compute the tangent space even when the guess passed to the realization routine is already a solution.
After `Assembly::realize` calls `engine::realize_gram`, it saves the returned tangent space in the assembly's `tangent` signal. The basis vectors are stored in configuration matrix format, ordered according to the elements' column indices. To help maintain consistency between the storage layout of the tangent space and the elements' column indices, we switch the column index data type from `usize` to `Option<usize>` and enforce the following invariants:
1. If an element has a column index, its tangent motions can be found in that column of the tangent space basis matrices.
2. If an element is affected by a constraint, it has a column index.
The comments in `assembly.rs` state the invariants and describe how they're enforced.
#### Automated testing
The test `engine::tests::tangent_test` builds a simple assembly with a known tangent space, runs the realization routine, and checks the returned tangent space against a hand-computed basis.
#### Limitations
The method `ConfigSubspace::symmetric_kernel` approximates the kernel by taking all the eigenspaces whose eigenvalues are smaller than a hard-coded threshold size. We may need a more flexible system eventually.
### Deformation
#### Implementation
The main purpose of this implementation is to confirm that deformation works as we'd hoped. The code is messy, and the deformation routine has at least one numerical quirk.
For simplicity, the keyboard commands that manipulate the assembly are handled by the display, just like the keyboard commands that control the camera. Deformation happens at the beginning of the animation loop.
The function `Assembly::deform` works like this:
1. Take a list of element motions
2. Project them onto the tangent space of the solution variety
3. Sum them to get a deformation $v$ of the whole assembly
4. Step the assembly along the "mass shell" geodesic tangent to $v$
* This step stays on the solution variety to first order
5. Call `realize` to bring the assembly back onto the solution variety
#### Manual testing
To manipulate the assembly:
1. Select a sphere
2. Make sure the display has focus
3. Hold the following keys:
* **A**/**D** for $x$ translation
* **W**/**S** for $y$ translation
* **shift**+**W**/**S** for $z$ translation
#### Limitations
Because the manipulation commands are handled by the display, you can only manipulate the assembly when the display has focus.
Since our test assemblies only include spheres, we assume in `Assembly::deform` that every element is a sphere.
When the tangent space is zero, `Assembly::deform` does nothing except print "The assembly is rigid" to the console.
During a deformation, the curvature and co-curvature components of a sphere's vector representation can exhibit weird discontinuous "swaps" that don't visibly affect how the sphere is drawn. *[I'll write more about this in an issue.]*
Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/29
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2024-12-30 22:53:07 +00:00
|
|
|
// manipulation
|
|
|
|
let translate_neg_x = create_signal(0.0);
|
|
|
|
let translate_pos_x = create_signal(0.0);
|
|
|
|
let translate_neg_y = create_signal(0.0);
|
|
|
|
let translate_pos_y = create_signal(0.0);
|
|
|
|
let translate_neg_z = create_signal(0.0);
|
|
|
|
let translate_pos_z = create_signal(0.0);
|
Switch to Euclidean-invariant projection onto tangent space of solution variety (#34)
This pull request addresses issues #32 and #33 by projecting nudges onto the tangent space of the solution variety using a Euclidean-invariant inner product, which I'm calling the *uniform* inner product.
### Definition of the uniform inner product
For spheres and planes, the uniform inner product is defined on the tangent space of the hyperboloid $\langle v, v \rangle = 1$. For points, it's defined on the tangent space of the paraboloid $\langle v, v \rangle = 0,\; \langle v, I_\infty \rangle = 1$.
The tangent space of an assembly can be expressed as the direct sum of the tangent spaces of the elements. We extend the uniform inner product to assemblies by declaring the tangent spaces of different elements to be orthogonal.
#### For spheres and planes
If $v = [x, y, z, b, c]^\top$ is on the hyperboloid $\langle v, v \rangle = 1$, the vectors
$$\left[ \begin{array}{c} 2b \\ \cdot \\ \cdot \\ \cdot \\ x \end{array} \right],\;\left[ \begin{array}{c} \cdot \\ 2b \\ \cdot \\ \cdot \\ y \end{array} \right],\;\left[ \begin{array}{c} \cdot \\ \cdot \\ 2b \\ \cdot \\ z \end{array} \right],\;\left[ \begin{array}{l} 2bx \\ 2by \\ 2bz \\ 2b^2 \\ 2bc + 1 \end{array} \right]$$
form a basis for the tangent space of hyperboloid at $v$. We declare this basis to be orthonormal with respect to the uniform inner product.
The first three vectors in the basis are unit-speed translations along the coordinate axes. The last vector moves the surface at unit speed along its normal field. For spheres, this increases the radius at unit rate. For planes, this translates the plane parallel to itself at unit speed. This description makes it clear that the uniform inner product is invariant under Euclidean motions.
#### For points
If $v = [x, y, z, b, c]^\top$ is on the paraboloid $\langle v, v \rangle = 0,\; \langle v, I_\infty \rangle = 1$, the vectors
$$\left[ \begin{array}{c} 2b \\ \cdot \\ \cdot \\ \cdot \\ x \end{array} \right],\;\left[ \begin{array}{c} \cdot \\ 2b \\ \cdot \\ \cdot \\ y \end{array} \right],\;\left[ \begin{array}{c} \cdot \\ \cdot \\ 2b \\ \cdot \\ z \end{array} \right]$$
form a basis for the tangent space of paraboloid at $v$. We declare this basis to be orthonormal with respect to the uniform inner product.
The meanings of the basis vectors, and the argument that the uniform inner product is Euclidean-invariant, are the same as for spheres and planes. In the engine, we pad the basis with $[0, 0, 0, 0, 1]^\top$ to keep the number of uniform coordinates consistent across element types.
### Confirmation of intended behavior
Two new tests confirm that we've corrected the misbehaviors described in issues #32 and #33.
Issue | Test
---|---
#32 | `proj_equivar_test`
#33 | `tangent_test_kaleidocycle`
Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/34
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2025-01-31 19:34:33 +00:00
|
|
|
let shrink_neg = create_signal(0.0);
|
|
|
|
let shrink_pos = create_signal(0.0);
|
Manipulate the assembly (#29)
feat: Find tangent space of solution variety, use for perturbations
### Tangent space
#### Implementation
The structure `engine::ConfigSubspace` represents a subspace of the configuration vector space $\operatorname{Hom}(\mathbb{R}^n, \mathbb{R}^5)$. It holds a basis for the subspace which is orthonormal with respect to the Euclidean inner product. The method `ConfigSubspace::symmetric_kernel` takes an endomorphism of the configuration vector space, which must be symmetric with respect to the Euclidean inner product, and returns its approximate kernel in the form of a `ConfigSubspace`.
At the end of `engine::realize_gram`, we use the computed Hessian to find the tangent space of the solution variety, and we return it alongside the realization. Since altering the constraints can change the tangent space without changing the solution, we compute the tangent space even when the guess passed to the realization routine is already a solution.
After `Assembly::realize` calls `engine::realize_gram`, it saves the returned tangent space in the assembly's `tangent` signal. The basis vectors are stored in configuration matrix format, ordered according to the elements' column indices. To help maintain consistency between the storage layout of the tangent space and the elements' column indices, we switch the column index data type from `usize` to `Option<usize>` and enforce the following invariants:
1. If an element has a column index, its tangent motions can be found in that column of the tangent space basis matrices.
2. If an element is affected by a constraint, it has a column index.
The comments in `assembly.rs` state the invariants and describe how they're enforced.
#### Automated testing
The test `engine::tests::tangent_test` builds a simple assembly with a known tangent space, runs the realization routine, and checks the returned tangent space against a hand-computed basis.
#### Limitations
The method `ConfigSubspace::symmetric_kernel` approximates the kernel by taking all the eigenspaces whose eigenvalues are smaller than a hard-coded threshold size. We may need a more flexible system eventually.
### Deformation
#### Implementation
The main purpose of this implementation is to confirm that deformation works as we'd hoped. The code is messy, and the deformation routine has at least one numerical quirk.
For simplicity, the keyboard commands that manipulate the assembly are handled by the display, just like the keyboard commands that control the camera. Deformation happens at the beginning of the animation loop.
The function `Assembly::deform` works like this:
1. Take a list of element motions
2. Project them onto the tangent space of the solution variety
3. Sum them to get a deformation $v$ of the whole assembly
4. Step the assembly along the "mass shell" geodesic tangent to $v$
* This step stays on the solution variety to first order
5. Call `realize` to bring the assembly back onto the solution variety
#### Manual testing
To manipulate the assembly:
1. Select a sphere
2. Make sure the display has focus
3. Hold the following keys:
* **A**/**D** for $x$ translation
* **W**/**S** for $y$ translation
* **shift**+**W**/**S** for $z$ translation
#### Limitations
Because the manipulation commands are handled by the display, you can only manipulate the assembly when the display has focus.
Since our test assemblies only include spheres, we assume in `Assembly::deform` that every element is a sphere.
When the tangent space is zero, `Assembly::deform` does nothing except print "The assembly is rigid" to the console.
During a deformation, the curvature and co-curvature components of a sphere's vector representation can exhibit weird discontinuous "swaps" that don't visibly affect how the sphere is drawn. *[I'll write more about this in an issue.]*
Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/29
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2024-12-30 22:53:07 +00:00
|
|
|
|
2024-10-21 23:38:27 +00:00
|
|
|
// change listener
|
|
|
|
let scene_changed = create_signal(true);
|
|
|
|
create_effect(move || {
|
2024-11-15 03:32:47 +00:00
|
|
|
state.assembly.elements.with(|elts| {
|
2025-05-06 19:17:30 +00:00
|
|
|
for elt in elts {
|
2025-05-01 19:25:13 +00:00
|
|
|
elt.representation().track();
|
2025-06-02 15:56:06 +00:00
|
|
|
elt.ghost().track();
|
2024-11-15 03:32:47 +00:00
|
|
|
}
|
|
|
|
});
|
2024-10-21 23:38:27 +00:00
|
|
|
state.selection.track();
|
|
|
|
scene_changed.set(true);
|
|
|
|
});
|
|
|
|
|
|
|
|
/* INSTRUMENTS */
|
|
|
|
const SAMPLE_PERIOD: i32 = 60;
|
|
|
|
let mut last_sample_time = 0.0;
|
|
|
|
let mut frames_since_last_sample = 0;
|
|
|
|
let mean_frame_interval = create_signal(0.0);
|
|
|
|
|
Manipulate the assembly (#29)
feat: Find tangent space of solution variety, use for perturbations
### Tangent space
#### Implementation
The structure `engine::ConfigSubspace` represents a subspace of the configuration vector space $\operatorname{Hom}(\mathbb{R}^n, \mathbb{R}^5)$. It holds a basis for the subspace which is orthonormal with respect to the Euclidean inner product. The method `ConfigSubspace::symmetric_kernel` takes an endomorphism of the configuration vector space, which must be symmetric with respect to the Euclidean inner product, and returns its approximate kernel in the form of a `ConfigSubspace`.
At the end of `engine::realize_gram`, we use the computed Hessian to find the tangent space of the solution variety, and we return it alongside the realization. Since altering the constraints can change the tangent space without changing the solution, we compute the tangent space even when the guess passed to the realization routine is already a solution.
After `Assembly::realize` calls `engine::realize_gram`, it saves the returned tangent space in the assembly's `tangent` signal. The basis vectors are stored in configuration matrix format, ordered according to the elements' column indices. To help maintain consistency between the storage layout of the tangent space and the elements' column indices, we switch the column index data type from `usize` to `Option<usize>` and enforce the following invariants:
1. If an element has a column index, its tangent motions can be found in that column of the tangent space basis matrices.
2. If an element is affected by a constraint, it has a column index.
The comments in `assembly.rs` state the invariants and describe how they're enforced.
#### Automated testing
The test `engine::tests::tangent_test` builds a simple assembly with a known tangent space, runs the realization routine, and checks the returned tangent space against a hand-computed basis.
#### Limitations
The method `ConfigSubspace::symmetric_kernel` approximates the kernel by taking all the eigenspaces whose eigenvalues are smaller than a hard-coded threshold size. We may need a more flexible system eventually.
### Deformation
#### Implementation
The main purpose of this implementation is to confirm that deformation works as we'd hoped. The code is messy, and the deformation routine has at least one numerical quirk.
For simplicity, the keyboard commands that manipulate the assembly are handled by the display, just like the keyboard commands that control the camera. Deformation happens at the beginning of the animation loop.
The function `Assembly::deform` works like this:
1. Take a list of element motions
2. Project them onto the tangent space of the solution variety
3. Sum them to get a deformation $v$ of the whole assembly
4. Step the assembly along the "mass shell" geodesic tangent to $v$
* This step stays on the solution variety to first order
5. Call `realize` to bring the assembly back onto the solution variety
#### Manual testing
To manipulate the assembly:
1. Select a sphere
2. Make sure the display has focus
3. Hold the following keys:
* **A**/**D** for $x$ translation
* **W**/**S** for $y$ translation
* **shift**+**W**/**S** for $z$ translation
#### Limitations
Because the manipulation commands are handled by the display, you can only manipulate the assembly when the display has focus.
Since our test assemblies only include spheres, we assume in `Assembly::deform` that every element is a sphere.
When the tangent space is zero, `Assembly::deform` does nothing except print "The assembly is rigid" to the console.
During a deformation, the curvature and co-curvature components of a sphere's vector representation can exhibit weird discontinuous "swaps" that don't visibly affect how the sphere is drawn. *[I'll write more about this in an issue.]*
Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/29
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2024-12-30 22:53:07 +00:00
|
|
|
let assembly_for_raf = state.assembly.clone();
|
2024-10-21 23:38:27 +00:00
|
|
|
on_mount(move || {
|
|
|
|
// timing
|
|
|
|
let mut last_time = 0.0;
|
|
|
|
|
|
|
|
// viewpoint
|
|
|
|
const ROT_SPEED: f64 = 0.4; // in radians per second
|
|
|
|
const ZOOM_SPEED: f64 = 0.15; // multiplicative rate per second
|
|
|
|
const TURNTABLE_SPEED: f64 = 0.1; /* BENCHMARKING */
|
|
|
|
let mut orientation = DMatrix::<f64>::identity(5, 5);
|
|
|
|
let mut rotation = DMatrix::<f64>::identity(5, 5);
|
|
|
|
let mut location_z: f64 = 5.0;
|
|
|
|
|
Manipulate the assembly (#29)
feat: Find tangent space of solution variety, use for perturbations
### Tangent space
#### Implementation
The structure `engine::ConfigSubspace` represents a subspace of the configuration vector space $\operatorname{Hom}(\mathbb{R}^n, \mathbb{R}^5)$. It holds a basis for the subspace which is orthonormal with respect to the Euclidean inner product. The method `ConfigSubspace::symmetric_kernel` takes an endomorphism of the configuration vector space, which must be symmetric with respect to the Euclidean inner product, and returns its approximate kernel in the form of a `ConfigSubspace`.
At the end of `engine::realize_gram`, we use the computed Hessian to find the tangent space of the solution variety, and we return it alongside the realization. Since altering the constraints can change the tangent space without changing the solution, we compute the tangent space even when the guess passed to the realization routine is already a solution.
After `Assembly::realize` calls `engine::realize_gram`, it saves the returned tangent space in the assembly's `tangent` signal. The basis vectors are stored in configuration matrix format, ordered according to the elements' column indices. To help maintain consistency between the storage layout of the tangent space and the elements' column indices, we switch the column index data type from `usize` to `Option<usize>` and enforce the following invariants:
1. If an element has a column index, its tangent motions can be found in that column of the tangent space basis matrices.
2. If an element is affected by a constraint, it has a column index.
The comments in `assembly.rs` state the invariants and describe how they're enforced.
#### Automated testing
The test `engine::tests::tangent_test` builds a simple assembly with a known tangent space, runs the realization routine, and checks the returned tangent space against a hand-computed basis.
#### Limitations
The method `ConfigSubspace::symmetric_kernel` approximates the kernel by taking all the eigenspaces whose eigenvalues are smaller than a hard-coded threshold size. We may need a more flexible system eventually.
### Deformation
#### Implementation
The main purpose of this implementation is to confirm that deformation works as we'd hoped. The code is messy, and the deformation routine has at least one numerical quirk.
For simplicity, the keyboard commands that manipulate the assembly are handled by the display, just like the keyboard commands that control the camera. Deformation happens at the beginning of the animation loop.
The function `Assembly::deform` works like this:
1. Take a list of element motions
2. Project them onto the tangent space of the solution variety
3. Sum them to get a deformation $v$ of the whole assembly
4. Step the assembly along the "mass shell" geodesic tangent to $v$
* This step stays on the solution variety to first order
5. Call `realize` to bring the assembly back onto the solution variety
#### Manual testing
To manipulate the assembly:
1. Select a sphere
2. Make sure the display has focus
3. Hold the following keys:
* **A**/**D** for $x$ translation
* **W**/**S** for $y$ translation
* **shift**+**W**/**S** for $z$ translation
#### Limitations
Because the manipulation commands are handled by the display, you can only manipulate the assembly when the display has focus.
Since our test assemblies only include spheres, we assume in `Assembly::deform` that every element is a sphere.
When the tangent space is zero, `Assembly::deform` does nothing except print "The assembly is rigid" to the console.
During a deformation, the curvature and co-curvature components of a sphere's vector representation can exhibit weird discontinuous "swaps" that don't visibly affect how the sphere is drawn. *[I'll write more about this in an issue.]*
Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/29
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2024-12-30 22:53:07 +00:00
|
|
|
// manipulation
|
|
|
|
const TRANSLATION_SPEED: f64 = 0.15; // in length units per second
|
Switch to Euclidean-invariant projection onto tangent space of solution variety (#34)
This pull request addresses issues #32 and #33 by projecting nudges onto the tangent space of the solution variety using a Euclidean-invariant inner product, which I'm calling the *uniform* inner product.
### Definition of the uniform inner product
For spheres and planes, the uniform inner product is defined on the tangent space of the hyperboloid $\langle v, v \rangle = 1$. For points, it's defined on the tangent space of the paraboloid $\langle v, v \rangle = 0,\; \langle v, I_\infty \rangle = 1$.
The tangent space of an assembly can be expressed as the direct sum of the tangent spaces of the elements. We extend the uniform inner product to assemblies by declaring the tangent spaces of different elements to be orthogonal.
#### For spheres and planes
If $v = [x, y, z, b, c]^\top$ is on the hyperboloid $\langle v, v \rangle = 1$, the vectors
$$\left[ \begin{array}{c} 2b \\ \cdot \\ \cdot \\ \cdot \\ x \end{array} \right],\;\left[ \begin{array}{c} \cdot \\ 2b \\ \cdot \\ \cdot \\ y \end{array} \right],\;\left[ \begin{array}{c} \cdot \\ \cdot \\ 2b \\ \cdot \\ z \end{array} \right],\;\left[ \begin{array}{l} 2bx \\ 2by \\ 2bz \\ 2b^2 \\ 2bc + 1 \end{array} \right]$$
form a basis for the tangent space of hyperboloid at $v$. We declare this basis to be orthonormal with respect to the uniform inner product.
The first three vectors in the basis are unit-speed translations along the coordinate axes. The last vector moves the surface at unit speed along its normal field. For spheres, this increases the radius at unit rate. For planes, this translates the plane parallel to itself at unit speed. This description makes it clear that the uniform inner product is invariant under Euclidean motions.
#### For points
If $v = [x, y, z, b, c]^\top$ is on the paraboloid $\langle v, v \rangle = 0,\; \langle v, I_\infty \rangle = 1$, the vectors
$$\left[ \begin{array}{c} 2b \\ \cdot \\ \cdot \\ \cdot \\ x \end{array} \right],\;\left[ \begin{array}{c} \cdot \\ 2b \\ \cdot \\ \cdot \\ y \end{array} \right],\;\left[ \begin{array}{c} \cdot \\ \cdot \\ 2b \\ \cdot \\ z \end{array} \right]$$
form a basis for the tangent space of paraboloid at $v$. We declare this basis to be orthonormal with respect to the uniform inner product.
The meanings of the basis vectors, and the argument that the uniform inner product is Euclidean-invariant, are the same as for spheres and planes. In the engine, we pad the basis with $[0, 0, 0, 0, 1]^\top$ to keep the number of uniform coordinates consistent across element types.
### Confirmation of intended behavior
Two new tests confirm that we've corrected the misbehaviors described in issues #32 and #33.
Issue | Test
---|---
#32 | `proj_equivar_test`
#33 | `tangent_test_kaleidocycle`
Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/34
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2025-01-31 19:34:33 +00:00
|
|
|
const SHRINKING_SPEED: f64 = 0.15; // in length units per second
|
Manipulate the assembly (#29)
feat: Find tangent space of solution variety, use for perturbations
### Tangent space
#### Implementation
The structure `engine::ConfigSubspace` represents a subspace of the configuration vector space $\operatorname{Hom}(\mathbb{R}^n, \mathbb{R}^5)$. It holds a basis for the subspace which is orthonormal with respect to the Euclidean inner product. The method `ConfigSubspace::symmetric_kernel` takes an endomorphism of the configuration vector space, which must be symmetric with respect to the Euclidean inner product, and returns its approximate kernel in the form of a `ConfigSubspace`.
At the end of `engine::realize_gram`, we use the computed Hessian to find the tangent space of the solution variety, and we return it alongside the realization. Since altering the constraints can change the tangent space without changing the solution, we compute the tangent space even when the guess passed to the realization routine is already a solution.
After `Assembly::realize` calls `engine::realize_gram`, it saves the returned tangent space in the assembly's `tangent` signal. The basis vectors are stored in configuration matrix format, ordered according to the elements' column indices. To help maintain consistency between the storage layout of the tangent space and the elements' column indices, we switch the column index data type from `usize` to `Option<usize>` and enforce the following invariants:
1. If an element has a column index, its tangent motions can be found in that column of the tangent space basis matrices.
2. If an element is affected by a constraint, it has a column index.
The comments in `assembly.rs` state the invariants and describe how they're enforced.
#### Automated testing
The test `engine::tests::tangent_test` builds a simple assembly with a known tangent space, runs the realization routine, and checks the returned tangent space against a hand-computed basis.
#### Limitations
The method `ConfigSubspace::symmetric_kernel` approximates the kernel by taking all the eigenspaces whose eigenvalues are smaller than a hard-coded threshold size. We may need a more flexible system eventually.
### Deformation
#### Implementation
The main purpose of this implementation is to confirm that deformation works as we'd hoped. The code is messy, and the deformation routine has at least one numerical quirk.
For simplicity, the keyboard commands that manipulate the assembly are handled by the display, just like the keyboard commands that control the camera. Deformation happens at the beginning of the animation loop.
The function `Assembly::deform` works like this:
1. Take a list of element motions
2. Project them onto the tangent space of the solution variety
3. Sum them to get a deformation $v$ of the whole assembly
4. Step the assembly along the "mass shell" geodesic tangent to $v$
* This step stays on the solution variety to first order
5. Call `realize` to bring the assembly back onto the solution variety
#### Manual testing
To manipulate the assembly:
1. Select a sphere
2. Make sure the display has focus
3. Hold the following keys:
* **A**/**D** for $x$ translation
* **W**/**S** for $y$ translation
* **shift**+**W**/**S** for $z$ translation
#### Limitations
Because the manipulation commands are handled by the display, you can only manipulate the assembly when the display has focus.
Since our test assemblies only include spheres, we assume in `Assembly::deform` that every element is a sphere.
When the tangent space is zero, `Assembly::deform` does nothing except print "The assembly is rigid" to the console.
During a deformation, the curvature and co-curvature components of a sphere's vector representation can exhibit weird discontinuous "swaps" that don't visibly affect how the sphere is drawn. *[I'll write more about this in an issue.]*
Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/29
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2024-12-30 22:53:07 +00:00
|
|
|
|
2024-10-21 23:38:27 +00:00
|
|
|
// display parameters
|
|
|
|
const LAYER_THRESHOLD: i32 = 0; /* DEBUG */
|
|
|
|
const DEBUG_MODE: i32 = 0; /* DEBUG */
|
|
|
|
|
|
|
|
/* INSTRUMENTS */
|
|
|
|
let performance = window().unwrap().performance().unwrap();
|
|
|
|
|
|
|
|
// get the display canvas
|
|
|
|
let canvas = display.get().unchecked_into::<web_sys::HtmlCanvasElement>();
|
|
|
|
let ctx = canvas
|
|
|
|
.get_context("webgl2")
|
|
|
|
.unwrap()
|
|
|
|
.unwrap()
|
|
|
|
.dyn_into::<WebGl2RenderingContext>()
|
|
|
|
.unwrap();
|
|
|
|
|
2025-05-01 19:25:13 +00:00
|
|
|
// disable depth testing
|
|
|
|
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(
|
2024-10-21 23:38:27 +00:00
|
|
|
&ctx,
|
|
|
|
include_str!("identity.vert"),
|
2025-05-01 19:25:13 +00:00
|
|
|
include_str!("spheres.frag")
|
2024-10-21 23:38:27 +00:00
|
|
|
);
|
2025-05-01 19:25:13 +00:00
|
|
|
|
|
|
|
// set up the point rendering program
|
|
|
|
let point_program = set_up_program(
|
2024-10-21 23:38:27 +00:00
|
|
|
&ctx,
|
2025-05-01 19:25:13 +00:00
|
|
|
include_str!("point.vert"),
|
|
|
|
include_str!("point.frag")
|
2024-10-21 23:38:27 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
/* DEBUG */
|
|
|
|
// print the maximum number of vectors that can be passed as
|
|
|
|
// uniforms to a fragment shader. the OpenGL ES 3.0 standard
|
|
|
|
// requires this maximum to be at least 224, as discussed in the
|
|
|
|
// documentation of the GL_MAX_FRAGMENT_UNIFORM_VECTORS parameter
|
|
|
|
// here:
|
|
|
|
//
|
|
|
|
// https://registry.khronos.org/OpenGL-Refpages/es3.0/html/glGet.xhtml
|
|
|
|
//
|
|
|
|
// there are also other size limits. for example, on Aaron's
|
|
|
|
// machine, the the length of a float or genType array seems to be
|
|
|
|
// capped at 1024 elements
|
|
|
|
console::log_2(
|
|
|
|
&ctx.get_parameter(WebGl2RenderingContext::MAX_FRAGMENT_UNIFORM_VECTORS).unwrap(),
|
|
|
|
&JsValue::from("uniform vectors available")
|
|
|
|
);
|
|
|
|
|
2025-05-01 19:25:13 +00:00
|
|
|
// 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
|
2024-10-21 23:38:27 +00:00
|
|
|
const SPHERE_MAX: usize = 200;
|
2025-05-01 19:25:13 +00:00
|
|
|
let sphere_cnt_loc = ctx.get_uniform_location(&sphere_program, "sphere_cnt");
|
2024-10-21 23:38:27 +00:00
|
|
|
let sphere_sp_locs = get_uniform_array_locations::<SPHERE_MAX>(
|
2025-05-01 19:25:13 +00:00
|
|
|
&ctx, &sphere_program, "sphere_list", Some("sp")
|
2024-10-21 23:38:27 +00:00
|
|
|
);
|
|
|
|
let sphere_lt_locs = get_uniform_array_locations::<SPHERE_MAX>(
|
2025-05-01 19:25:13 +00:00
|
|
|
&ctx, &sphere_program, "sphere_list", Some("lt")
|
2024-10-21 23:38:27 +00:00
|
|
|
);
|
2025-05-01 19:25:13 +00:00
|
|
|
let sphere_color_locs = get_uniform_array_locations::<SPHERE_MAX>(
|
|
|
|
&ctx, &sphere_program, "color_list", None
|
2024-10-21 23:38:27 +00:00
|
|
|
);
|
2025-05-01 19:25:13 +00:00
|
|
|
let sphere_highlight_locs = get_uniform_array_locations::<SPHERE_MAX>(
|
|
|
|
&ctx, &sphere_program, "highlight_list", None
|
2024-10-21 23:38:27 +00:00
|
|
|
);
|
2025-05-01 19:25:13 +00:00
|
|
|
let resolution_loc = ctx.get_uniform_location(&sphere_program, "resolution");
|
|
|
|
let shortdim_loc = ctx.get_uniform_location(&sphere_program, "shortdim");
|
|
|
|
let layer_threshold_loc = ctx.get_uniform_location(&sphere_program, "layer_threshold");
|
|
|
|
let debug_mode_loc = ctx.get_uniform_location(&sphere_program, "debug_mode");
|
2024-10-21 23:38:27 +00:00
|
|
|
|
2025-05-01 19:25:13 +00:00
|
|
|
// load the viewport vertex positions into a new vertex buffer object
|
2024-10-21 23:38:27 +00:00
|
|
|
const VERTEX_CNT: usize = 6;
|
2025-05-01 19:25:13 +00:00
|
|
|
let viewport_positions: [f32; 3*VERTEX_CNT] = [
|
2024-10-21 23:38:27 +00:00
|
|
|
// northwest triangle
|
|
|
|
-1.0, -1.0, 0.0,
|
|
|
|
-1.0, 1.0, 0.0,
|
|
|
|
1.0, 1.0, 0.0,
|
|
|
|
// southeast triangle
|
|
|
|
-1.0, -1.0, 0.0,
|
|
|
|
1.0, 1.0, 0.0,
|
|
|
|
1.0, -1.0, 0.0
|
|
|
|
];
|
2025-05-01 19:25:13 +00:00
|
|
|
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;
|
2024-10-21 23:38:27 +00:00
|
|
|
|
|
|
|
// set up a repainting routine
|
|
|
|
let (_, start_animation_loop, _) = create_raf(move || {
|
|
|
|
// get the time step
|
|
|
|
let time = performance.now();
|
|
|
|
let time_step = 0.001*(time - last_time);
|
|
|
|
last_time = time;
|
|
|
|
|
|
|
|
// get the navigation state
|
|
|
|
let pitch_up_val = pitch_up.get();
|
|
|
|
let pitch_down_val = pitch_down.get();
|
|
|
|
let yaw_right_val = yaw_right.get();
|
|
|
|
let yaw_left_val = yaw_left.get();
|
|
|
|
let roll_ccw_val = roll_ccw.get();
|
|
|
|
let roll_cw_val = roll_cw.get();
|
|
|
|
let zoom_in_val = zoom_in.get();
|
|
|
|
let zoom_out_val = zoom_out.get();
|
|
|
|
let turntable_val = turntable.get(); /* BENCHMARKING */
|
|
|
|
|
Manipulate the assembly (#29)
feat: Find tangent space of solution variety, use for perturbations
### Tangent space
#### Implementation
The structure `engine::ConfigSubspace` represents a subspace of the configuration vector space $\operatorname{Hom}(\mathbb{R}^n, \mathbb{R}^5)$. It holds a basis for the subspace which is orthonormal with respect to the Euclidean inner product. The method `ConfigSubspace::symmetric_kernel` takes an endomorphism of the configuration vector space, which must be symmetric with respect to the Euclidean inner product, and returns its approximate kernel in the form of a `ConfigSubspace`.
At the end of `engine::realize_gram`, we use the computed Hessian to find the tangent space of the solution variety, and we return it alongside the realization. Since altering the constraints can change the tangent space without changing the solution, we compute the tangent space even when the guess passed to the realization routine is already a solution.
After `Assembly::realize` calls `engine::realize_gram`, it saves the returned tangent space in the assembly's `tangent` signal. The basis vectors are stored in configuration matrix format, ordered according to the elements' column indices. To help maintain consistency between the storage layout of the tangent space and the elements' column indices, we switch the column index data type from `usize` to `Option<usize>` and enforce the following invariants:
1. If an element has a column index, its tangent motions can be found in that column of the tangent space basis matrices.
2. If an element is affected by a constraint, it has a column index.
The comments in `assembly.rs` state the invariants and describe how they're enforced.
#### Automated testing
The test `engine::tests::tangent_test` builds a simple assembly with a known tangent space, runs the realization routine, and checks the returned tangent space against a hand-computed basis.
#### Limitations
The method `ConfigSubspace::symmetric_kernel` approximates the kernel by taking all the eigenspaces whose eigenvalues are smaller than a hard-coded threshold size. We may need a more flexible system eventually.
### Deformation
#### Implementation
The main purpose of this implementation is to confirm that deformation works as we'd hoped. The code is messy, and the deformation routine has at least one numerical quirk.
For simplicity, the keyboard commands that manipulate the assembly are handled by the display, just like the keyboard commands that control the camera. Deformation happens at the beginning of the animation loop.
The function `Assembly::deform` works like this:
1. Take a list of element motions
2. Project them onto the tangent space of the solution variety
3. Sum them to get a deformation $v$ of the whole assembly
4. Step the assembly along the "mass shell" geodesic tangent to $v$
* This step stays on the solution variety to first order
5. Call `realize` to bring the assembly back onto the solution variety
#### Manual testing
To manipulate the assembly:
1. Select a sphere
2. Make sure the display has focus
3. Hold the following keys:
* **A**/**D** for $x$ translation
* **W**/**S** for $y$ translation
* **shift**+**W**/**S** for $z$ translation
#### Limitations
Because the manipulation commands are handled by the display, you can only manipulate the assembly when the display has focus.
Since our test assemblies only include spheres, we assume in `Assembly::deform` that every element is a sphere.
When the tangent space is zero, `Assembly::deform` does nothing except print "The assembly is rigid" to the console.
During a deformation, the curvature and co-curvature components of a sphere's vector representation can exhibit weird discontinuous "swaps" that don't visibly affect how the sphere is drawn. *[I'll write more about this in an issue.]*
Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/29
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2024-12-30 22:53:07 +00:00
|
|
|
// get the manipulation state
|
|
|
|
let translate_neg_x_val = translate_neg_x.get();
|
|
|
|
let translate_pos_x_val = translate_pos_x.get();
|
|
|
|
let translate_neg_y_val = translate_neg_y.get();
|
|
|
|
let translate_pos_y_val = translate_pos_y.get();
|
|
|
|
let translate_neg_z_val = translate_neg_z.get();
|
|
|
|
let translate_pos_z_val = translate_pos_z.get();
|
Switch to Euclidean-invariant projection onto tangent space of solution variety (#34)
This pull request addresses issues #32 and #33 by projecting nudges onto the tangent space of the solution variety using a Euclidean-invariant inner product, which I'm calling the *uniform* inner product.
### Definition of the uniform inner product
For spheres and planes, the uniform inner product is defined on the tangent space of the hyperboloid $\langle v, v \rangle = 1$. For points, it's defined on the tangent space of the paraboloid $\langle v, v \rangle = 0,\; \langle v, I_\infty \rangle = 1$.
The tangent space of an assembly can be expressed as the direct sum of the tangent spaces of the elements. We extend the uniform inner product to assemblies by declaring the tangent spaces of different elements to be orthogonal.
#### For spheres and planes
If $v = [x, y, z, b, c]^\top$ is on the hyperboloid $\langle v, v \rangle = 1$, the vectors
$$\left[ \begin{array}{c} 2b \\ \cdot \\ \cdot \\ \cdot \\ x \end{array} \right],\;\left[ \begin{array}{c} \cdot \\ 2b \\ \cdot \\ \cdot \\ y \end{array} \right],\;\left[ \begin{array}{c} \cdot \\ \cdot \\ 2b \\ \cdot \\ z \end{array} \right],\;\left[ \begin{array}{l} 2bx \\ 2by \\ 2bz \\ 2b^2 \\ 2bc + 1 \end{array} \right]$$
form a basis for the tangent space of hyperboloid at $v$. We declare this basis to be orthonormal with respect to the uniform inner product.
The first three vectors in the basis are unit-speed translations along the coordinate axes. The last vector moves the surface at unit speed along its normal field. For spheres, this increases the radius at unit rate. For planes, this translates the plane parallel to itself at unit speed. This description makes it clear that the uniform inner product is invariant under Euclidean motions.
#### For points
If $v = [x, y, z, b, c]^\top$ is on the paraboloid $\langle v, v \rangle = 0,\; \langle v, I_\infty \rangle = 1$, the vectors
$$\left[ \begin{array}{c} 2b \\ \cdot \\ \cdot \\ \cdot \\ x \end{array} \right],\;\left[ \begin{array}{c} \cdot \\ 2b \\ \cdot \\ \cdot \\ y \end{array} \right],\;\left[ \begin{array}{c} \cdot \\ \cdot \\ 2b \\ \cdot \\ z \end{array} \right]$$
form a basis for the tangent space of paraboloid at $v$. We declare this basis to be orthonormal with respect to the uniform inner product.
The meanings of the basis vectors, and the argument that the uniform inner product is Euclidean-invariant, are the same as for spheres and planes. In the engine, we pad the basis with $[0, 0, 0, 0, 1]^\top$ to keep the number of uniform coordinates consistent across element types.
### Confirmation of intended behavior
Two new tests confirm that we've corrected the misbehaviors described in issues #32 and #33.
Issue | Test
---|---
#32 | `proj_equivar_test`
#33 | `tangent_test_kaleidocycle`
Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/34
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2025-01-31 19:34:33 +00:00
|
|
|
let shrink_neg_val = shrink_neg.get();
|
|
|
|
let shrink_pos_val = shrink_pos.get();
|
Manipulate the assembly (#29)
feat: Find tangent space of solution variety, use for perturbations
### Tangent space
#### Implementation
The structure `engine::ConfigSubspace` represents a subspace of the configuration vector space $\operatorname{Hom}(\mathbb{R}^n, \mathbb{R}^5)$. It holds a basis for the subspace which is orthonormal with respect to the Euclidean inner product. The method `ConfigSubspace::symmetric_kernel` takes an endomorphism of the configuration vector space, which must be symmetric with respect to the Euclidean inner product, and returns its approximate kernel in the form of a `ConfigSubspace`.
At the end of `engine::realize_gram`, we use the computed Hessian to find the tangent space of the solution variety, and we return it alongside the realization. Since altering the constraints can change the tangent space without changing the solution, we compute the tangent space even when the guess passed to the realization routine is already a solution.
After `Assembly::realize` calls `engine::realize_gram`, it saves the returned tangent space in the assembly's `tangent` signal. The basis vectors are stored in configuration matrix format, ordered according to the elements' column indices. To help maintain consistency between the storage layout of the tangent space and the elements' column indices, we switch the column index data type from `usize` to `Option<usize>` and enforce the following invariants:
1. If an element has a column index, its tangent motions can be found in that column of the tangent space basis matrices.
2. If an element is affected by a constraint, it has a column index.
The comments in `assembly.rs` state the invariants and describe how they're enforced.
#### Automated testing
The test `engine::tests::tangent_test` builds a simple assembly with a known tangent space, runs the realization routine, and checks the returned tangent space against a hand-computed basis.
#### Limitations
The method `ConfigSubspace::symmetric_kernel` approximates the kernel by taking all the eigenspaces whose eigenvalues are smaller than a hard-coded threshold size. We may need a more flexible system eventually.
### Deformation
#### Implementation
The main purpose of this implementation is to confirm that deformation works as we'd hoped. The code is messy, and the deformation routine has at least one numerical quirk.
For simplicity, the keyboard commands that manipulate the assembly are handled by the display, just like the keyboard commands that control the camera. Deformation happens at the beginning of the animation loop.
The function `Assembly::deform` works like this:
1. Take a list of element motions
2. Project them onto the tangent space of the solution variety
3. Sum them to get a deformation $v$ of the whole assembly
4. Step the assembly along the "mass shell" geodesic tangent to $v$
* This step stays on the solution variety to first order
5. Call `realize` to bring the assembly back onto the solution variety
#### Manual testing
To manipulate the assembly:
1. Select a sphere
2. Make sure the display has focus
3. Hold the following keys:
* **A**/**D** for $x$ translation
* **W**/**S** for $y$ translation
* **shift**+**W**/**S** for $z$ translation
#### Limitations
Because the manipulation commands are handled by the display, you can only manipulate the assembly when the display has focus.
Since our test assemblies only include spheres, we assume in `Assembly::deform` that every element is a sphere.
When the tangent space is zero, `Assembly::deform` does nothing except print "The assembly is rigid" to the console.
During a deformation, the curvature and co-curvature components of a sphere's vector representation can exhibit weird discontinuous "swaps" that don't visibly affect how the sphere is drawn. *[I'll write more about this in an issue.]*
Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/29
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2024-12-30 22:53:07 +00:00
|
|
|
|
2024-10-21 23:38:27 +00:00
|
|
|
// update the assembly's orientation
|
|
|
|
let ang_vel = {
|
|
|
|
let pitch = pitch_up_val - pitch_down_val;
|
|
|
|
let yaw = yaw_right_val - yaw_left_val;
|
|
|
|
let roll = roll_ccw_val - roll_cw_val;
|
|
|
|
if pitch != 0.0 || yaw != 0.0 || roll != 0.0 {
|
|
|
|
ROT_SPEED * Vector3::new(-pitch, yaw, roll).normalize()
|
|
|
|
} else {
|
|
|
|
Vector3::zeros()
|
|
|
|
}
|
|
|
|
} /* BENCHMARKING */ + if turntable_val {
|
|
|
|
Vector3::new(0.0, TURNTABLE_SPEED, 0.0)
|
|
|
|
} else {
|
|
|
|
Vector3::zeros()
|
|
|
|
};
|
|
|
|
let mut rotation_sp = rotation.fixed_view_mut::<3, 3>(0, 0);
|
|
|
|
rotation_sp.copy_from(
|
|
|
|
Rotation3::from_scaled_axis(time_step * ang_vel).matrix()
|
|
|
|
);
|
|
|
|
orientation = &rotation * &orientation;
|
|
|
|
|
|
|
|
// update the assembly's location
|
|
|
|
let zoom = zoom_out_val - zoom_in_val;
|
|
|
|
location_z *= (time_step * ZOOM_SPEED * zoom).exp();
|
|
|
|
|
Manipulate the assembly (#29)
feat: Find tangent space of solution variety, use for perturbations
### Tangent space
#### Implementation
The structure `engine::ConfigSubspace` represents a subspace of the configuration vector space $\operatorname{Hom}(\mathbb{R}^n, \mathbb{R}^5)$. It holds a basis for the subspace which is orthonormal with respect to the Euclidean inner product. The method `ConfigSubspace::symmetric_kernel` takes an endomorphism of the configuration vector space, which must be symmetric with respect to the Euclidean inner product, and returns its approximate kernel in the form of a `ConfigSubspace`.
At the end of `engine::realize_gram`, we use the computed Hessian to find the tangent space of the solution variety, and we return it alongside the realization. Since altering the constraints can change the tangent space without changing the solution, we compute the tangent space even when the guess passed to the realization routine is already a solution.
After `Assembly::realize` calls `engine::realize_gram`, it saves the returned tangent space in the assembly's `tangent` signal. The basis vectors are stored in configuration matrix format, ordered according to the elements' column indices. To help maintain consistency between the storage layout of the tangent space and the elements' column indices, we switch the column index data type from `usize` to `Option<usize>` and enforce the following invariants:
1. If an element has a column index, its tangent motions can be found in that column of the tangent space basis matrices.
2. If an element is affected by a constraint, it has a column index.
The comments in `assembly.rs` state the invariants and describe how they're enforced.
#### Automated testing
The test `engine::tests::tangent_test` builds a simple assembly with a known tangent space, runs the realization routine, and checks the returned tangent space against a hand-computed basis.
#### Limitations
The method `ConfigSubspace::symmetric_kernel` approximates the kernel by taking all the eigenspaces whose eigenvalues are smaller than a hard-coded threshold size. We may need a more flexible system eventually.
### Deformation
#### Implementation
The main purpose of this implementation is to confirm that deformation works as we'd hoped. The code is messy, and the deformation routine has at least one numerical quirk.
For simplicity, the keyboard commands that manipulate the assembly are handled by the display, just like the keyboard commands that control the camera. Deformation happens at the beginning of the animation loop.
The function `Assembly::deform` works like this:
1. Take a list of element motions
2. Project them onto the tangent space of the solution variety
3. Sum them to get a deformation $v$ of the whole assembly
4. Step the assembly along the "mass shell" geodesic tangent to $v$
* This step stays on the solution variety to first order
5. Call `realize` to bring the assembly back onto the solution variety
#### Manual testing
To manipulate the assembly:
1. Select a sphere
2. Make sure the display has focus
3. Hold the following keys:
* **A**/**D** for $x$ translation
* **W**/**S** for $y$ translation
* **shift**+**W**/**S** for $z$ translation
#### Limitations
Because the manipulation commands are handled by the display, you can only manipulate the assembly when the display has focus.
Since our test assemblies only include spheres, we assume in `Assembly::deform` that every element is a sphere.
When the tangent space is zero, `Assembly::deform` does nothing except print "The assembly is rigid" to the console.
During a deformation, the curvature and co-curvature components of a sphere's vector representation can exhibit weird discontinuous "swaps" that don't visibly affect how the sphere is drawn. *[I'll write more about this in an issue.]*
Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/29
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2024-12-30 22:53:07 +00:00
|
|
|
// manipulate the assembly
|
|
|
|
if state.selection.with(|sel| sel.len() == 1) {
|
|
|
|
let sel = state.selection.with(
|
2025-05-06 19:17:30 +00:00
|
|
|
|sel| sel.into_iter().next().unwrap().clone()
|
Manipulate the assembly (#29)
feat: Find tangent space of solution variety, use for perturbations
### Tangent space
#### Implementation
The structure `engine::ConfigSubspace` represents a subspace of the configuration vector space $\operatorname{Hom}(\mathbb{R}^n, \mathbb{R}^5)$. It holds a basis for the subspace which is orthonormal with respect to the Euclidean inner product. The method `ConfigSubspace::symmetric_kernel` takes an endomorphism of the configuration vector space, which must be symmetric with respect to the Euclidean inner product, and returns its approximate kernel in the form of a `ConfigSubspace`.
At the end of `engine::realize_gram`, we use the computed Hessian to find the tangent space of the solution variety, and we return it alongside the realization. Since altering the constraints can change the tangent space without changing the solution, we compute the tangent space even when the guess passed to the realization routine is already a solution.
After `Assembly::realize` calls `engine::realize_gram`, it saves the returned tangent space in the assembly's `tangent` signal. The basis vectors are stored in configuration matrix format, ordered according to the elements' column indices. To help maintain consistency between the storage layout of the tangent space and the elements' column indices, we switch the column index data type from `usize` to `Option<usize>` and enforce the following invariants:
1. If an element has a column index, its tangent motions can be found in that column of the tangent space basis matrices.
2. If an element is affected by a constraint, it has a column index.
The comments in `assembly.rs` state the invariants and describe how they're enforced.
#### Automated testing
The test `engine::tests::tangent_test` builds a simple assembly with a known tangent space, runs the realization routine, and checks the returned tangent space against a hand-computed basis.
#### Limitations
The method `ConfigSubspace::symmetric_kernel` approximates the kernel by taking all the eigenspaces whose eigenvalues are smaller than a hard-coded threshold size. We may need a more flexible system eventually.
### Deformation
#### Implementation
The main purpose of this implementation is to confirm that deformation works as we'd hoped. The code is messy, and the deformation routine has at least one numerical quirk.
For simplicity, the keyboard commands that manipulate the assembly are handled by the display, just like the keyboard commands that control the camera. Deformation happens at the beginning of the animation loop.
The function `Assembly::deform` works like this:
1. Take a list of element motions
2. Project them onto the tangent space of the solution variety
3. Sum them to get a deformation $v$ of the whole assembly
4. Step the assembly along the "mass shell" geodesic tangent to $v$
* This step stays on the solution variety to first order
5. Call `realize` to bring the assembly back onto the solution variety
#### Manual testing
To manipulate the assembly:
1. Select a sphere
2. Make sure the display has focus
3. Hold the following keys:
* **A**/**D** for $x$ translation
* **W**/**S** for $y$ translation
* **shift**+**W**/**S** for $z$ translation
#### Limitations
Because the manipulation commands are handled by the display, you can only manipulate the assembly when the display has focus.
Since our test assemblies only include spheres, we assume in `Assembly::deform` that every element is a sphere.
When the tangent space is zero, `Assembly::deform` does nothing except print "The assembly is rigid" to the console.
During a deformation, the curvature and co-curvature components of a sphere's vector representation can exhibit weird discontinuous "swaps" that don't visibly affect how the sphere is drawn. *[I'll write more about this in an issue.]*
Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/29
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2024-12-30 22:53:07 +00:00
|
|
|
);
|
|
|
|
let translate_x = translate_pos_x_val - translate_neg_x_val;
|
|
|
|
let translate_y = translate_pos_y_val - translate_neg_y_val;
|
|
|
|
let translate_z = translate_pos_z_val - translate_neg_z_val;
|
Switch to Euclidean-invariant projection onto tangent space of solution variety (#34)
This pull request addresses issues #32 and #33 by projecting nudges onto the tangent space of the solution variety using a Euclidean-invariant inner product, which I'm calling the *uniform* inner product.
### Definition of the uniform inner product
For spheres and planes, the uniform inner product is defined on the tangent space of the hyperboloid $\langle v, v \rangle = 1$. For points, it's defined on the tangent space of the paraboloid $\langle v, v \rangle = 0,\; \langle v, I_\infty \rangle = 1$.
The tangent space of an assembly can be expressed as the direct sum of the tangent spaces of the elements. We extend the uniform inner product to assemblies by declaring the tangent spaces of different elements to be orthogonal.
#### For spheres and planes
If $v = [x, y, z, b, c]^\top$ is on the hyperboloid $\langle v, v \rangle = 1$, the vectors
$$\left[ \begin{array}{c} 2b \\ \cdot \\ \cdot \\ \cdot \\ x \end{array} \right],\;\left[ \begin{array}{c} \cdot \\ 2b \\ \cdot \\ \cdot \\ y \end{array} \right],\;\left[ \begin{array}{c} \cdot \\ \cdot \\ 2b \\ \cdot \\ z \end{array} \right],\;\left[ \begin{array}{l} 2bx \\ 2by \\ 2bz \\ 2b^2 \\ 2bc + 1 \end{array} \right]$$
form a basis for the tangent space of hyperboloid at $v$. We declare this basis to be orthonormal with respect to the uniform inner product.
The first three vectors in the basis are unit-speed translations along the coordinate axes. The last vector moves the surface at unit speed along its normal field. For spheres, this increases the radius at unit rate. For planes, this translates the plane parallel to itself at unit speed. This description makes it clear that the uniform inner product is invariant under Euclidean motions.
#### For points
If $v = [x, y, z, b, c]^\top$ is on the paraboloid $\langle v, v \rangle = 0,\; \langle v, I_\infty \rangle = 1$, the vectors
$$\left[ \begin{array}{c} 2b \\ \cdot \\ \cdot \\ \cdot \\ x \end{array} \right],\;\left[ \begin{array}{c} \cdot \\ 2b \\ \cdot \\ \cdot \\ y \end{array} \right],\;\left[ \begin{array}{c} \cdot \\ \cdot \\ 2b \\ \cdot \\ z \end{array} \right]$$
form a basis for the tangent space of paraboloid at $v$. We declare this basis to be orthonormal with respect to the uniform inner product.
The meanings of the basis vectors, and the argument that the uniform inner product is Euclidean-invariant, are the same as for spheres and planes. In the engine, we pad the basis with $[0, 0, 0, 0, 1]^\top$ to keep the number of uniform coordinates consistent across element types.
### Confirmation of intended behavior
Two new tests confirm that we've corrected the misbehaviors described in issues #32 and #33.
Issue | Test
---|---
#32 | `proj_equivar_test`
#33 | `tangent_test_kaleidocycle`
Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/34
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2025-01-31 19:34:33 +00:00
|
|
|
let shrink = shrink_pos_val - shrink_neg_val;
|
|
|
|
let translating =
|
|
|
|
translate_x != 0.0
|
|
|
|
|| translate_y != 0.0
|
|
|
|
|| translate_z != 0.0;
|
|
|
|
if translating || shrink != 0.0 {
|
|
|
|
let elt_motion = {
|
|
|
|
let u = if translating {
|
|
|
|
TRANSLATION_SPEED * Vector3::new(
|
|
|
|
translate_x, translate_y, translate_z
|
|
|
|
).normalize()
|
|
|
|
} else {
|
|
|
|
Vector3::zeros()
|
|
|
|
};
|
|
|
|
time_step * DVector::from_column_slice(
|
|
|
|
&[u[0], u[1], u[2], SHRINKING_SPEED * shrink]
|
|
|
|
)
|
Manipulate the assembly (#29)
feat: Find tangent space of solution variety, use for perturbations
### Tangent space
#### Implementation
The structure `engine::ConfigSubspace` represents a subspace of the configuration vector space $\operatorname{Hom}(\mathbb{R}^n, \mathbb{R}^5)$. It holds a basis for the subspace which is orthonormal with respect to the Euclidean inner product. The method `ConfigSubspace::symmetric_kernel` takes an endomorphism of the configuration vector space, which must be symmetric with respect to the Euclidean inner product, and returns its approximate kernel in the form of a `ConfigSubspace`.
At the end of `engine::realize_gram`, we use the computed Hessian to find the tangent space of the solution variety, and we return it alongside the realization. Since altering the constraints can change the tangent space without changing the solution, we compute the tangent space even when the guess passed to the realization routine is already a solution.
After `Assembly::realize` calls `engine::realize_gram`, it saves the returned tangent space in the assembly's `tangent` signal. The basis vectors are stored in configuration matrix format, ordered according to the elements' column indices. To help maintain consistency between the storage layout of the tangent space and the elements' column indices, we switch the column index data type from `usize` to `Option<usize>` and enforce the following invariants:
1. If an element has a column index, its tangent motions can be found in that column of the tangent space basis matrices.
2. If an element is affected by a constraint, it has a column index.
The comments in `assembly.rs` state the invariants and describe how they're enforced.
#### Automated testing
The test `engine::tests::tangent_test` builds a simple assembly with a known tangent space, runs the realization routine, and checks the returned tangent space against a hand-computed basis.
#### Limitations
The method `ConfigSubspace::symmetric_kernel` approximates the kernel by taking all the eigenspaces whose eigenvalues are smaller than a hard-coded threshold size. We may need a more flexible system eventually.
### Deformation
#### Implementation
The main purpose of this implementation is to confirm that deformation works as we'd hoped. The code is messy, and the deformation routine has at least one numerical quirk.
For simplicity, the keyboard commands that manipulate the assembly are handled by the display, just like the keyboard commands that control the camera. Deformation happens at the beginning of the animation loop.
The function `Assembly::deform` works like this:
1. Take a list of element motions
2. Project them onto the tangent space of the solution variety
3. Sum them to get a deformation $v$ of the whole assembly
4. Step the assembly along the "mass shell" geodesic tangent to $v$
* This step stays on the solution variety to first order
5. Call `realize` to bring the assembly back onto the solution variety
#### Manual testing
To manipulate the assembly:
1. Select a sphere
2. Make sure the display has focus
3. Hold the following keys:
* **A**/**D** for $x$ translation
* **W**/**S** for $y$ translation
* **shift**+**W**/**S** for $z$ translation
#### Limitations
Because the manipulation commands are handled by the display, you can only manipulate the assembly when the display has focus.
Since our test assemblies only include spheres, we assume in `Assembly::deform` that every element is a sphere.
When the tangent space is zero, `Assembly::deform` does nothing except print "The assembly is rigid" to the console.
During a deformation, the curvature and co-curvature components of a sphere's vector representation can exhibit weird discontinuous "swaps" that don't visibly affect how the sphere is drawn. *[I'll write more about this in an issue.]*
Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/29
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2024-12-30 22:53:07 +00:00
|
|
|
};
|
|
|
|
assembly_for_raf.deform(
|
|
|
|
vec![
|
|
|
|
ElementMotion {
|
2025-05-06 19:17:30 +00:00
|
|
|
element: sel,
|
Manipulate the assembly (#29)
feat: Find tangent space of solution variety, use for perturbations
### Tangent space
#### Implementation
The structure `engine::ConfigSubspace` represents a subspace of the configuration vector space $\operatorname{Hom}(\mathbb{R}^n, \mathbb{R}^5)$. It holds a basis for the subspace which is orthonormal with respect to the Euclidean inner product. The method `ConfigSubspace::symmetric_kernel` takes an endomorphism of the configuration vector space, which must be symmetric with respect to the Euclidean inner product, and returns its approximate kernel in the form of a `ConfigSubspace`.
At the end of `engine::realize_gram`, we use the computed Hessian to find the tangent space of the solution variety, and we return it alongside the realization. Since altering the constraints can change the tangent space without changing the solution, we compute the tangent space even when the guess passed to the realization routine is already a solution.
After `Assembly::realize` calls `engine::realize_gram`, it saves the returned tangent space in the assembly's `tangent` signal. The basis vectors are stored in configuration matrix format, ordered according to the elements' column indices. To help maintain consistency between the storage layout of the tangent space and the elements' column indices, we switch the column index data type from `usize` to `Option<usize>` and enforce the following invariants:
1. If an element has a column index, its tangent motions can be found in that column of the tangent space basis matrices.
2. If an element is affected by a constraint, it has a column index.
The comments in `assembly.rs` state the invariants and describe how they're enforced.
#### Automated testing
The test `engine::tests::tangent_test` builds a simple assembly with a known tangent space, runs the realization routine, and checks the returned tangent space against a hand-computed basis.
#### Limitations
The method `ConfigSubspace::symmetric_kernel` approximates the kernel by taking all the eigenspaces whose eigenvalues are smaller than a hard-coded threshold size. We may need a more flexible system eventually.
### Deformation
#### Implementation
The main purpose of this implementation is to confirm that deformation works as we'd hoped. The code is messy, and the deformation routine has at least one numerical quirk.
For simplicity, the keyboard commands that manipulate the assembly are handled by the display, just like the keyboard commands that control the camera. Deformation happens at the beginning of the animation loop.
The function `Assembly::deform` works like this:
1. Take a list of element motions
2. Project them onto the tangent space of the solution variety
3. Sum them to get a deformation $v$ of the whole assembly
4. Step the assembly along the "mass shell" geodesic tangent to $v$
* This step stays on the solution variety to first order
5. Call `realize` to bring the assembly back onto the solution variety
#### Manual testing
To manipulate the assembly:
1. Select a sphere
2. Make sure the display has focus
3. Hold the following keys:
* **A**/**D** for $x$ translation
* **W**/**S** for $y$ translation
* **shift**+**W**/**S** for $z$ translation
#### Limitations
Because the manipulation commands are handled by the display, you can only manipulate the assembly when the display has focus.
Since our test assemblies only include spheres, we assume in `Assembly::deform` that every element is a sphere.
When the tangent space is zero, `Assembly::deform` does nothing except print "The assembly is rigid" to the console.
During a deformation, the curvature and co-curvature components of a sphere's vector representation can exhibit weird discontinuous "swaps" that don't visibly affect how the sphere is drawn. *[I'll write more about this in an issue.]*
Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/29
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2024-12-30 22:53:07 +00:00
|
|
|
velocity: elt_motion.as_view()
|
|
|
|
}
|
|
|
|
]
|
|
|
|
);
|
|
|
|
scene_changed.set(true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-21 23:38:27 +00:00
|
|
|
if scene_changed.get() {
|
2025-05-01 19:25:13 +00:00
|
|
|
const SPACE_DIM: usize = 3;
|
|
|
|
const COLOR_SIZE: usize = 3;
|
|
|
|
|
2024-10-21 23:38:27 +00:00
|
|
|
/* INSTRUMENTS */
|
|
|
|
// measure mean frame interval
|
|
|
|
frames_since_last_sample += 1;
|
|
|
|
if frames_since_last_sample >= SAMPLE_PERIOD {
|
|
|
|
mean_frame_interval.set((time - last_sample_time) / (SAMPLE_PERIOD as f64));
|
|
|
|
last_sample_time = time;
|
|
|
|
frames_since_last_sample = 0;
|
|
|
|
}
|
|
|
|
|
2025-05-01 19:25:13 +00:00
|
|
|
// --- get the assembly ---
|
|
|
|
|
|
|
|
let mut scene = Scene::new();
|
|
|
|
|
2024-10-21 23:38:27 +00:00
|
|
|
// find the map from assembly space to world space
|
|
|
|
let location = {
|
|
|
|
let u = -location_z;
|
|
|
|
DMatrix::from_column_slice(5, 5, &[
|
|
|
|
1.0, 0.0, 0.0, 0.0, 0.0,
|
|
|
|
0.0, 1.0, 0.0, 0.0, 0.0,
|
|
|
|
0.0, 0.0, 1.0, 0.0, u,
|
|
|
|
0.0, 0.0, 2.0*u, 1.0, u*u,
|
|
|
|
0.0, 0.0, 0.0, 0.0, 1.0
|
|
|
|
])
|
|
|
|
};
|
2024-11-27 05:02:06 +00:00
|
|
|
let asm_to_world = &location * &orientation;
|
2024-10-21 23:38:27 +00:00
|
|
|
|
2025-05-01 19:25:13 +00:00
|
|
|
// set up the scene
|
|
|
|
state.assembly.elements.with_untracked(
|
2025-05-06 19:17:30 +00:00
|
|
|
|elts| for elt in elts {
|
|
|
|
let selected = state.selection.with(|sel| sel.contains(elt));
|
2025-05-01 19:25:13 +00:00
|
|
|
elt.show(&mut scene, selected);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
let sphere_cnt = scene.spheres.len_i32();
|
|
|
|
|
|
|
|
// --- draw the spheres ---
|
|
|
|
|
|
|
|
// use the sphere rendering program
|
|
|
|
ctx.use_program(Some(&sphere_program));
|
|
|
|
|
|
|
|
// enable the sphere program's vertex attribute
|
|
|
|
ctx.enable_vertex_attrib_array(viewport_position_attr);
|
|
|
|
|
|
|
|
// write the spheres in world coordinates
|
|
|
|
let sphere_reps_world: Vec<_> = scene.spheres.representations.into_iter().map(
|
|
|
|
|rep| (&asm_to_world * rep).cast::<f32>()
|
|
|
|
).collect();
|
2024-10-21 23:38:27 +00:00
|
|
|
|
|
|
|
// set the resolution
|
|
|
|
let width = canvas.width() as f32;
|
|
|
|
let height = canvas.height() as f32;
|
|
|
|
ctx.uniform2f(resolution_loc.as_ref(), width, height);
|
|
|
|
ctx.uniform1f(shortdim_loc.as_ref(), width.min(height));
|
|
|
|
|
2025-05-01 19:25:13 +00:00
|
|
|
// pass the scene data
|
|
|
|
ctx.uniform1i(sphere_cnt_loc.as_ref(), sphere_cnt);
|
|
|
|
for n in 0..sphere_reps_world.len() {
|
|
|
|
let v = &sphere_reps_world[n];
|
|
|
|
ctx.uniform3fv_with_f32_array(
|
2024-10-21 23:38:27 +00:00
|
|
|
sphere_sp_locs[n].as_ref(),
|
2025-05-01 19:25:13 +00:00
|
|
|
v.rows(0, 3).as_slice()
|
2024-10-21 23:38:27 +00:00
|
|
|
);
|
2025-05-01 19:25:13 +00:00
|
|
|
ctx.uniform2fv_with_f32_array(
|
2024-10-21 23:38:27 +00:00
|
|
|
sphere_lt_locs[n].as_ref(),
|
2025-05-01 19:25:13 +00:00
|
|
|
v.rows(3, 2).as_slice()
|
2024-10-21 23:38:27 +00:00
|
|
|
);
|
2025-06-02 15:56:06 +00:00
|
|
|
ctx.uniform4fv_with_f32_array(
|
2025-05-01 19:25:13 +00:00
|
|
|
sphere_color_locs[n].as_ref(),
|
2025-06-02 15:56:06 +00:00
|
|
|
&scene.spheres.colors_with_opacity[n]
|
2024-10-21 23:38:27 +00:00
|
|
|
);
|
|
|
|
ctx.uniform1f(
|
2025-05-01 19:25:13 +00:00
|
|
|
sphere_highlight_locs[n].as_ref(),
|
|
|
|
scene.spheres.highlights[n]
|
2024-10-21 23:38:27 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// pass the display parameters
|
|
|
|
ctx.uniform1i(layer_threshold_loc.as_ref(), LAYER_THRESHOLD);
|
|
|
|
ctx.uniform1i(debug_mode_loc.as_ref(), DEBUG_MODE);
|
|
|
|
|
2025-05-01 19:25:13 +00:00
|
|
|
// 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);
|
|
|
|
|
2024-10-21 23:38:27 +00:00
|
|
|
// draw the scene
|
|
|
|
ctx.draw_arrays(WebGl2RenderingContext::TRIANGLES, 0, VERTEX_CNT as i32);
|
|
|
|
|
2025-05-01 19:25:13 +00:00
|
|
|
// 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());
|
2025-06-02 15:56:06 +00:00
|
|
|
bind_new_buffer_to_attribute(&ctx, point_color_attr, (COLOR_SIZE + 1) as i32, scene.points.colors_with_opacity.concat().as_slice());
|
2025-05-01 19:25:13 +00:00
|
|
|
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 ---
|
|
|
|
|
2024-11-27 05:02:06 +00:00
|
|
|
// update the viewpoint
|
|
|
|
assembly_to_world.set(asm_to_world);
|
|
|
|
|
2024-10-21 23:38:27 +00:00
|
|
|
// clear the scene change flag
|
|
|
|
scene_changed.set(
|
|
|
|
pitch_up_val != 0.0
|
|
|
|
|| pitch_down_val != 0.0
|
|
|
|
|| yaw_left_val != 0.0
|
|
|
|
|| yaw_right_val != 0.0
|
|
|
|
|| roll_cw_val != 0.0
|
|
|
|
|| roll_ccw_val != 0.0
|
|
|
|
|| zoom_in_val != 0.0
|
|
|
|
|| zoom_out_val != 0.0
|
|
|
|
|| turntable_val /* BENCHMARKING */
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
frames_since_last_sample = 0;
|
|
|
|
mean_frame_interval.set(-1.0);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
start_animation_loop();
|
|
|
|
});
|
|
|
|
|
Manipulate the assembly (#29)
feat: Find tangent space of solution variety, use for perturbations
### Tangent space
#### Implementation
The structure `engine::ConfigSubspace` represents a subspace of the configuration vector space $\operatorname{Hom}(\mathbb{R}^n, \mathbb{R}^5)$. It holds a basis for the subspace which is orthonormal with respect to the Euclidean inner product. The method `ConfigSubspace::symmetric_kernel` takes an endomorphism of the configuration vector space, which must be symmetric with respect to the Euclidean inner product, and returns its approximate kernel in the form of a `ConfigSubspace`.
At the end of `engine::realize_gram`, we use the computed Hessian to find the tangent space of the solution variety, and we return it alongside the realization. Since altering the constraints can change the tangent space without changing the solution, we compute the tangent space even when the guess passed to the realization routine is already a solution.
After `Assembly::realize` calls `engine::realize_gram`, it saves the returned tangent space in the assembly's `tangent` signal. The basis vectors are stored in configuration matrix format, ordered according to the elements' column indices. To help maintain consistency between the storage layout of the tangent space and the elements' column indices, we switch the column index data type from `usize` to `Option<usize>` and enforce the following invariants:
1. If an element has a column index, its tangent motions can be found in that column of the tangent space basis matrices.
2. If an element is affected by a constraint, it has a column index.
The comments in `assembly.rs` state the invariants and describe how they're enforced.
#### Automated testing
The test `engine::tests::tangent_test` builds a simple assembly with a known tangent space, runs the realization routine, and checks the returned tangent space against a hand-computed basis.
#### Limitations
The method `ConfigSubspace::symmetric_kernel` approximates the kernel by taking all the eigenspaces whose eigenvalues are smaller than a hard-coded threshold size. We may need a more flexible system eventually.
### Deformation
#### Implementation
The main purpose of this implementation is to confirm that deformation works as we'd hoped. The code is messy, and the deformation routine has at least one numerical quirk.
For simplicity, the keyboard commands that manipulate the assembly are handled by the display, just like the keyboard commands that control the camera. Deformation happens at the beginning of the animation loop.
The function `Assembly::deform` works like this:
1. Take a list of element motions
2. Project them onto the tangent space of the solution variety
3. Sum them to get a deformation $v$ of the whole assembly
4. Step the assembly along the "mass shell" geodesic tangent to $v$
* This step stays on the solution variety to first order
5. Call `realize` to bring the assembly back onto the solution variety
#### Manual testing
To manipulate the assembly:
1. Select a sphere
2. Make sure the display has focus
3. Hold the following keys:
* **A**/**D** for $x$ translation
* **W**/**S** for $y$ translation
* **shift**+**W**/**S** for $z$ translation
#### Limitations
Because the manipulation commands are handled by the display, you can only manipulate the assembly when the display has focus.
Since our test assemblies only include spheres, we assume in `Assembly::deform` that every element is a sphere.
When the tangent space is zero, `Assembly::deform` does nothing except print "The assembly is rigid" to the console.
During a deformation, the curvature and co-curvature components of a sphere's vector representation can exhibit weird discontinuous "swaps" that don't visibly affect how the sphere is drawn. *[I'll write more about this in an issue.]*
Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/29
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2024-12-30 22:53:07 +00:00
|
|
|
let set_nav_signal = move |event: &KeyboardEvent, value: f64| {
|
2024-10-21 23:38:27 +00:00
|
|
|
let mut navigating = true;
|
|
|
|
let shift = event.shift_key();
|
|
|
|
match event.key().as_str() {
|
|
|
|
"ArrowUp" if shift => zoom_in.set(value),
|
|
|
|
"ArrowDown" if shift => zoom_out.set(value),
|
|
|
|
"ArrowUp" => pitch_up.set(value),
|
|
|
|
"ArrowDown" => pitch_down.set(value),
|
|
|
|
"ArrowRight" if shift => roll_cw.set(value),
|
|
|
|
"ArrowLeft" if shift => roll_ccw.set(value),
|
|
|
|
"ArrowRight" => yaw_right.set(value),
|
|
|
|
"ArrowLeft" => yaw_left.set(value),
|
|
|
|
_ => navigating = false
|
|
|
|
};
|
|
|
|
if navigating {
|
|
|
|
scene_changed.set(true);
|
|
|
|
event.prevent_default();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
Manipulate the assembly (#29)
feat: Find tangent space of solution variety, use for perturbations
### Tangent space
#### Implementation
The structure `engine::ConfigSubspace` represents a subspace of the configuration vector space $\operatorname{Hom}(\mathbb{R}^n, \mathbb{R}^5)$. It holds a basis for the subspace which is orthonormal with respect to the Euclidean inner product. The method `ConfigSubspace::symmetric_kernel` takes an endomorphism of the configuration vector space, which must be symmetric with respect to the Euclidean inner product, and returns its approximate kernel in the form of a `ConfigSubspace`.
At the end of `engine::realize_gram`, we use the computed Hessian to find the tangent space of the solution variety, and we return it alongside the realization. Since altering the constraints can change the tangent space without changing the solution, we compute the tangent space even when the guess passed to the realization routine is already a solution.
After `Assembly::realize` calls `engine::realize_gram`, it saves the returned tangent space in the assembly's `tangent` signal. The basis vectors are stored in configuration matrix format, ordered according to the elements' column indices. To help maintain consistency between the storage layout of the tangent space and the elements' column indices, we switch the column index data type from `usize` to `Option<usize>` and enforce the following invariants:
1. If an element has a column index, its tangent motions can be found in that column of the tangent space basis matrices.
2. If an element is affected by a constraint, it has a column index.
The comments in `assembly.rs` state the invariants and describe how they're enforced.
#### Automated testing
The test `engine::tests::tangent_test` builds a simple assembly with a known tangent space, runs the realization routine, and checks the returned tangent space against a hand-computed basis.
#### Limitations
The method `ConfigSubspace::symmetric_kernel` approximates the kernel by taking all the eigenspaces whose eigenvalues are smaller than a hard-coded threshold size. We may need a more flexible system eventually.
### Deformation
#### Implementation
The main purpose of this implementation is to confirm that deformation works as we'd hoped. The code is messy, and the deformation routine has at least one numerical quirk.
For simplicity, the keyboard commands that manipulate the assembly are handled by the display, just like the keyboard commands that control the camera. Deformation happens at the beginning of the animation loop.
The function `Assembly::deform` works like this:
1. Take a list of element motions
2. Project them onto the tangent space of the solution variety
3. Sum them to get a deformation $v$ of the whole assembly
4. Step the assembly along the "mass shell" geodesic tangent to $v$
* This step stays on the solution variety to first order
5. Call `realize` to bring the assembly back onto the solution variety
#### Manual testing
To manipulate the assembly:
1. Select a sphere
2. Make sure the display has focus
3. Hold the following keys:
* **A**/**D** for $x$ translation
* **W**/**S** for $y$ translation
* **shift**+**W**/**S** for $z$ translation
#### Limitations
Because the manipulation commands are handled by the display, you can only manipulate the assembly when the display has focus.
Since our test assemblies only include spheres, we assume in `Assembly::deform` that every element is a sphere.
When the tangent space is zero, `Assembly::deform` does nothing except print "The assembly is rigid" to the console.
During a deformation, the curvature and co-curvature components of a sphere's vector representation can exhibit weird discontinuous "swaps" that don't visibly affect how the sphere is drawn. *[I'll write more about this in an issue.]*
Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/29
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2024-12-30 22:53:07 +00:00
|
|
|
let set_manip_signal = move |event: &KeyboardEvent, value: f64| {
|
|
|
|
let mut manipulating = true;
|
|
|
|
let shift = event.shift_key();
|
|
|
|
match event.key().as_str() {
|
|
|
|
"d" | "D" => translate_pos_x.set(value),
|
|
|
|
"a" | "A" => translate_neg_x.set(value),
|
|
|
|
"w" | "W" if shift => translate_neg_z.set(value),
|
|
|
|
"s" | "S" if shift => translate_pos_z.set(value),
|
|
|
|
"w" | "W" => translate_pos_y.set(value),
|
|
|
|
"s" | "S" => translate_neg_y.set(value),
|
Switch to Euclidean-invariant projection onto tangent space of solution variety (#34)
This pull request addresses issues #32 and #33 by projecting nudges onto the tangent space of the solution variety using a Euclidean-invariant inner product, which I'm calling the *uniform* inner product.
### Definition of the uniform inner product
For spheres and planes, the uniform inner product is defined on the tangent space of the hyperboloid $\langle v, v \rangle = 1$. For points, it's defined on the tangent space of the paraboloid $\langle v, v \rangle = 0,\; \langle v, I_\infty \rangle = 1$.
The tangent space of an assembly can be expressed as the direct sum of the tangent spaces of the elements. We extend the uniform inner product to assemblies by declaring the tangent spaces of different elements to be orthogonal.
#### For spheres and planes
If $v = [x, y, z, b, c]^\top$ is on the hyperboloid $\langle v, v \rangle = 1$, the vectors
$$\left[ \begin{array}{c} 2b \\ \cdot \\ \cdot \\ \cdot \\ x \end{array} \right],\;\left[ \begin{array}{c} \cdot \\ 2b \\ \cdot \\ \cdot \\ y \end{array} \right],\;\left[ \begin{array}{c} \cdot \\ \cdot \\ 2b \\ \cdot \\ z \end{array} \right],\;\left[ \begin{array}{l} 2bx \\ 2by \\ 2bz \\ 2b^2 \\ 2bc + 1 \end{array} \right]$$
form a basis for the tangent space of hyperboloid at $v$. We declare this basis to be orthonormal with respect to the uniform inner product.
The first three vectors in the basis are unit-speed translations along the coordinate axes. The last vector moves the surface at unit speed along its normal field. For spheres, this increases the radius at unit rate. For planes, this translates the plane parallel to itself at unit speed. This description makes it clear that the uniform inner product is invariant under Euclidean motions.
#### For points
If $v = [x, y, z, b, c]^\top$ is on the paraboloid $\langle v, v \rangle = 0,\; \langle v, I_\infty \rangle = 1$, the vectors
$$\left[ \begin{array}{c} 2b \\ \cdot \\ \cdot \\ \cdot \\ x \end{array} \right],\;\left[ \begin{array}{c} \cdot \\ 2b \\ \cdot \\ \cdot \\ y \end{array} \right],\;\left[ \begin{array}{c} \cdot \\ \cdot \\ 2b \\ \cdot \\ z \end{array} \right]$$
form a basis for the tangent space of paraboloid at $v$. We declare this basis to be orthonormal with respect to the uniform inner product.
The meanings of the basis vectors, and the argument that the uniform inner product is Euclidean-invariant, are the same as for spheres and planes. In the engine, we pad the basis with $[0, 0, 0, 0, 1]^\top$ to keep the number of uniform coordinates consistent across element types.
### Confirmation of intended behavior
Two new tests confirm that we've corrected the misbehaviors described in issues #32 and #33.
Issue | Test
---|---
#32 | `proj_equivar_test`
#33 | `tangent_test_kaleidocycle`
Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/34
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2025-01-31 19:34:33 +00:00
|
|
|
"]" | "}" => shrink_neg.set(value),
|
|
|
|
"[" | "{" => shrink_pos.set(value),
|
Manipulate the assembly (#29)
feat: Find tangent space of solution variety, use for perturbations
### Tangent space
#### Implementation
The structure `engine::ConfigSubspace` represents a subspace of the configuration vector space $\operatorname{Hom}(\mathbb{R}^n, \mathbb{R}^5)$. It holds a basis for the subspace which is orthonormal with respect to the Euclidean inner product. The method `ConfigSubspace::symmetric_kernel` takes an endomorphism of the configuration vector space, which must be symmetric with respect to the Euclidean inner product, and returns its approximate kernel in the form of a `ConfigSubspace`.
At the end of `engine::realize_gram`, we use the computed Hessian to find the tangent space of the solution variety, and we return it alongside the realization. Since altering the constraints can change the tangent space without changing the solution, we compute the tangent space even when the guess passed to the realization routine is already a solution.
After `Assembly::realize` calls `engine::realize_gram`, it saves the returned tangent space in the assembly's `tangent` signal. The basis vectors are stored in configuration matrix format, ordered according to the elements' column indices. To help maintain consistency between the storage layout of the tangent space and the elements' column indices, we switch the column index data type from `usize` to `Option<usize>` and enforce the following invariants:
1. If an element has a column index, its tangent motions can be found in that column of the tangent space basis matrices.
2. If an element is affected by a constraint, it has a column index.
The comments in `assembly.rs` state the invariants and describe how they're enforced.
#### Automated testing
The test `engine::tests::tangent_test` builds a simple assembly with a known tangent space, runs the realization routine, and checks the returned tangent space against a hand-computed basis.
#### Limitations
The method `ConfigSubspace::symmetric_kernel` approximates the kernel by taking all the eigenspaces whose eigenvalues are smaller than a hard-coded threshold size. We may need a more flexible system eventually.
### Deformation
#### Implementation
The main purpose of this implementation is to confirm that deformation works as we'd hoped. The code is messy, and the deformation routine has at least one numerical quirk.
For simplicity, the keyboard commands that manipulate the assembly are handled by the display, just like the keyboard commands that control the camera. Deformation happens at the beginning of the animation loop.
The function `Assembly::deform` works like this:
1. Take a list of element motions
2. Project them onto the tangent space of the solution variety
3. Sum them to get a deformation $v$ of the whole assembly
4. Step the assembly along the "mass shell" geodesic tangent to $v$
* This step stays on the solution variety to first order
5. Call `realize` to bring the assembly back onto the solution variety
#### Manual testing
To manipulate the assembly:
1. Select a sphere
2. Make sure the display has focus
3. Hold the following keys:
* **A**/**D** for $x$ translation
* **W**/**S** for $y$ translation
* **shift**+**W**/**S** for $z$ translation
#### Limitations
Because the manipulation commands are handled by the display, you can only manipulate the assembly when the display has focus.
Since our test assemblies only include spheres, we assume in `Assembly::deform` that every element is a sphere.
When the tangent space is zero, `Assembly::deform` does nothing except print "The assembly is rigid" to the console.
During a deformation, the curvature and co-curvature components of a sphere's vector representation can exhibit weird discontinuous "swaps" that don't visibly affect how the sphere is drawn. *[I'll write more about this in an issue.]*
Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/29
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2024-12-30 22:53:07 +00:00
|
|
|
_ => manipulating = false
|
|
|
|
};
|
|
|
|
if manipulating {
|
|
|
|
event.prevent_default();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2024-10-21 23:38:27 +00:00
|
|
|
view! {
|
|
|
|
/* TO DO */
|
|
|
|
// switch back to integer-valued parameters when that becomes possible
|
|
|
|
// again
|
|
|
|
canvas(
|
|
|
|
ref=display,
|
|
|
|
width="600",
|
|
|
|
height="600",
|
|
|
|
tabindex="0",
|
|
|
|
on:keydown=move |event: KeyboardEvent| {
|
|
|
|
if event.key() == "Shift" {
|
Manipulate the assembly (#29)
feat: Find tangent space of solution variety, use for perturbations
### Tangent space
#### Implementation
The structure `engine::ConfigSubspace` represents a subspace of the configuration vector space $\operatorname{Hom}(\mathbb{R}^n, \mathbb{R}^5)$. It holds a basis for the subspace which is orthonormal with respect to the Euclidean inner product. The method `ConfigSubspace::symmetric_kernel` takes an endomorphism of the configuration vector space, which must be symmetric with respect to the Euclidean inner product, and returns its approximate kernel in the form of a `ConfigSubspace`.
At the end of `engine::realize_gram`, we use the computed Hessian to find the tangent space of the solution variety, and we return it alongside the realization. Since altering the constraints can change the tangent space without changing the solution, we compute the tangent space even when the guess passed to the realization routine is already a solution.
After `Assembly::realize` calls `engine::realize_gram`, it saves the returned tangent space in the assembly's `tangent` signal. The basis vectors are stored in configuration matrix format, ordered according to the elements' column indices. To help maintain consistency between the storage layout of the tangent space and the elements' column indices, we switch the column index data type from `usize` to `Option<usize>` and enforce the following invariants:
1. If an element has a column index, its tangent motions can be found in that column of the tangent space basis matrices.
2. If an element is affected by a constraint, it has a column index.
The comments in `assembly.rs` state the invariants and describe how they're enforced.
#### Automated testing
The test `engine::tests::tangent_test` builds a simple assembly with a known tangent space, runs the realization routine, and checks the returned tangent space against a hand-computed basis.
#### Limitations
The method `ConfigSubspace::symmetric_kernel` approximates the kernel by taking all the eigenspaces whose eigenvalues are smaller than a hard-coded threshold size. We may need a more flexible system eventually.
### Deformation
#### Implementation
The main purpose of this implementation is to confirm that deformation works as we'd hoped. The code is messy, and the deformation routine has at least one numerical quirk.
For simplicity, the keyboard commands that manipulate the assembly are handled by the display, just like the keyboard commands that control the camera. Deformation happens at the beginning of the animation loop.
The function `Assembly::deform` works like this:
1. Take a list of element motions
2. Project them onto the tangent space of the solution variety
3. Sum them to get a deformation $v$ of the whole assembly
4. Step the assembly along the "mass shell" geodesic tangent to $v$
* This step stays on the solution variety to first order
5. Call `realize` to bring the assembly back onto the solution variety
#### Manual testing
To manipulate the assembly:
1. Select a sphere
2. Make sure the display has focus
3. Hold the following keys:
* **A**/**D** for $x$ translation
* **W**/**S** for $y$ translation
* **shift**+**W**/**S** for $z$ translation
#### Limitations
Because the manipulation commands are handled by the display, you can only manipulate the assembly when the display has focus.
Since our test assemblies only include spheres, we assume in `Assembly::deform` that every element is a sphere.
When the tangent space is zero, `Assembly::deform` does nothing except print "The assembly is rigid" to the console.
During a deformation, the curvature and co-curvature components of a sphere's vector representation can exhibit weird discontinuous "swaps" that don't visibly affect how the sphere is drawn. *[I'll write more about this in an issue.]*
Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/29
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2024-12-30 22:53:07 +00:00
|
|
|
// swap navigation inputs
|
2024-10-21 23:38:27 +00:00
|
|
|
roll_cw.set(yaw_right.get());
|
|
|
|
roll_ccw.set(yaw_left.get());
|
|
|
|
zoom_in.set(pitch_up.get());
|
|
|
|
zoom_out.set(pitch_down.get());
|
|
|
|
yaw_right.set(0.0);
|
|
|
|
yaw_left.set(0.0);
|
|
|
|
pitch_up.set(0.0);
|
|
|
|
pitch_down.set(0.0);
|
Manipulate the assembly (#29)
feat: Find tangent space of solution variety, use for perturbations
### Tangent space
#### Implementation
The structure `engine::ConfigSubspace` represents a subspace of the configuration vector space $\operatorname{Hom}(\mathbb{R}^n, \mathbb{R}^5)$. It holds a basis for the subspace which is orthonormal with respect to the Euclidean inner product. The method `ConfigSubspace::symmetric_kernel` takes an endomorphism of the configuration vector space, which must be symmetric with respect to the Euclidean inner product, and returns its approximate kernel in the form of a `ConfigSubspace`.
At the end of `engine::realize_gram`, we use the computed Hessian to find the tangent space of the solution variety, and we return it alongside the realization. Since altering the constraints can change the tangent space without changing the solution, we compute the tangent space even when the guess passed to the realization routine is already a solution.
After `Assembly::realize` calls `engine::realize_gram`, it saves the returned tangent space in the assembly's `tangent` signal. The basis vectors are stored in configuration matrix format, ordered according to the elements' column indices. To help maintain consistency between the storage layout of the tangent space and the elements' column indices, we switch the column index data type from `usize` to `Option<usize>` and enforce the following invariants:
1. If an element has a column index, its tangent motions can be found in that column of the tangent space basis matrices.
2. If an element is affected by a constraint, it has a column index.
The comments in `assembly.rs` state the invariants and describe how they're enforced.
#### Automated testing
The test `engine::tests::tangent_test` builds a simple assembly with a known tangent space, runs the realization routine, and checks the returned tangent space against a hand-computed basis.
#### Limitations
The method `ConfigSubspace::symmetric_kernel` approximates the kernel by taking all the eigenspaces whose eigenvalues are smaller than a hard-coded threshold size. We may need a more flexible system eventually.
### Deformation
#### Implementation
The main purpose of this implementation is to confirm that deformation works as we'd hoped. The code is messy, and the deformation routine has at least one numerical quirk.
For simplicity, the keyboard commands that manipulate the assembly are handled by the display, just like the keyboard commands that control the camera. Deformation happens at the beginning of the animation loop.
The function `Assembly::deform` works like this:
1. Take a list of element motions
2. Project them onto the tangent space of the solution variety
3. Sum them to get a deformation $v$ of the whole assembly
4. Step the assembly along the "mass shell" geodesic tangent to $v$
* This step stays on the solution variety to first order
5. Call `realize` to bring the assembly back onto the solution variety
#### Manual testing
To manipulate the assembly:
1. Select a sphere
2. Make sure the display has focus
3. Hold the following keys:
* **A**/**D** for $x$ translation
* **W**/**S** for $y$ translation
* **shift**+**W**/**S** for $z$ translation
#### Limitations
Because the manipulation commands are handled by the display, you can only manipulate the assembly when the display has focus.
Since our test assemblies only include spheres, we assume in `Assembly::deform` that every element is a sphere.
When the tangent space is zero, `Assembly::deform` does nothing except print "The assembly is rigid" to the console.
During a deformation, the curvature and co-curvature components of a sphere's vector representation can exhibit weird discontinuous "swaps" that don't visibly affect how the sphere is drawn. *[I'll write more about this in an issue.]*
Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/29
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2024-12-30 22:53:07 +00:00
|
|
|
|
|
|
|
// swap manipulation inputs
|
|
|
|
translate_pos_z.set(translate_neg_y.get());
|
|
|
|
translate_neg_z.set(translate_pos_y.get());
|
|
|
|
translate_pos_y.set(0.0);
|
|
|
|
translate_neg_y.set(0.0);
|
2024-10-21 23:38:27 +00:00
|
|
|
} else {
|
|
|
|
if event.key() == "Enter" { /* BENCHMARKING */
|
|
|
|
turntable.set_fn(|turn| !turn);
|
|
|
|
scene_changed.set(true);
|
|
|
|
}
|
Manipulate the assembly (#29)
feat: Find tangent space of solution variety, use for perturbations
### Tangent space
#### Implementation
The structure `engine::ConfigSubspace` represents a subspace of the configuration vector space $\operatorname{Hom}(\mathbb{R}^n, \mathbb{R}^5)$. It holds a basis for the subspace which is orthonormal with respect to the Euclidean inner product. The method `ConfigSubspace::symmetric_kernel` takes an endomorphism of the configuration vector space, which must be symmetric with respect to the Euclidean inner product, and returns its approximate kernel in the form of a `ConfigSubspace`.
At the end of `engine::realize_gram`, we use the computed Hessian to find the tangent space of the solution variety, and we return it alongside the realization. Since altering the constraints can change the tangent space without changing the solution, we compute the tangent space even when the guess passed to the realization routine is already a solution.
After `Assembly::realize` calls `engine::realize_gram`, it saves the returned tangent space in the assembly's `tangent` signal. The basis vectors are stored in configuration matrix format, ordered according to the elements' column indices. To help maintain consistency between the storage layout of the tangent space and the elements' column indices, we switch the column index data type from `usize` to `Option<usize>` and enforce the following invariants:
1. If an element has a column index, its tangent motions can be found in that column of the tangent space basis matrices.
2. If an element is affected by a constraint, it has a column index.
The comments in `assembly.rs` state the invariants and describe how they're enforced.
#### Automated testing
The test `engine::tests::tangent_test` builds a simple assembly with a known tangent space, runs the realization routine, and checks the returned tangent space against a hand-computed basis.
#### Limitations
The method `ConfigSubspace::symmetric_kernel` approximates the kernel by taking all the eigenspaces whose eigenvalues are smaller than a hard-coded threshold size. We may need a more flexible system eventually.
### Deformation
#### Implementation
The main purpose of this implementation is to confirm that deformation works as we'd hoped. The code is messy, and the deformation routine has at least one numerical quirk.
For simplicity, the keyboard commands that manipulate the assembly are handled by the display, just like the keyboard commands that control the camera. Deformation happens at the beginning of the animation loop.
The function `Assembly::deform` works like this:
1. Take a list of element motions
2. Project them onto the tangent space of the solution variety
3. Sum them to get a deformation $v$ of the whole assembly
4. Step the assembly along the "mass shell" geodesic tangent to $v$
* This step stays on the solution variety to first order
5. Call `realize` to bring the assembly back onto the solution variety
#### Manual testing
To manipulate the assembly:
1. Select a sphere
2. Make sure the display has focus
3. Hold the following keys:
* **A**/**D** for $x$ translation
* **W**/**S** for $y$ translation
* **shift**+**W**/**S** for $z$ translation
#### Limitations
Because the manipulation commands are handled by the display, you can only manipulate the assembly when the display has focus.
Since our test assemblies only include spheres, we assume in `Assembly::deform` that every element is a sphere.
When the tangent space is zero, `Assembly::deform` does nothing except print "The assembly is rigid" to the console.
During a deformation, the curvature and co-curvature components of a sphere's vector representation can exhibit weird discontinuous "swaps" that don't visibly affect how the sphere is drawn. *[I'll write more about this in an issue.]*
Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/29
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2024-12-30 22:53:07 +00:00
|
|
|
set_nav_signal(&event, 1.0);
|
|
|
|
set_manip_signal(&event, 1.0);
|
2024-10-21 23:38:27 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
on:keyup=move |event: KeyboardEvent| {
|
|
|
|
if event.key() == "Shift" {
|
Manipulate the assembly (#29)
feat: Find tangent space of solution variety, use for perturbations
### Tangent space
#### Implementation
The structure `engine::ConfigSubspace` represents a subspace of the configuration vector space $\operatorname{Hom}(\mathbb{R}^n, \mathbb{R}^5)$. It holds a basis for the subspace which is orthonormal with respect to the Euclidean inner product. The method `ConfigSubspace::symmetric_kernel` takes an endomorphism of the configuration vector space, which must be symmetric with respect to the Euclidean inner product, and returns its approximate kernel in the form of a `ConfigSubspace`.
At the end of `engine::realize_gram`, we use the computed Hessian to find the tangent space of the solution variety, and we return it alongside the realization. Since altering the constraints can change the tangent space without changing the solution, we compute the tangent space even when the guess passed to the realization routine is already a solution.
After `Assembly::realize` calls `engine::realize_gram`, it saves the returned tangent space in the assembly's `tangent` signal. The basis vectors are stored in configuration matrix format, ordered according to the elements' column indices. To help maintain consistency between the storage layout of the tangent space and the elements' column indices, we switch the column index data type from `usize` to `Option<usize>` and enforce the following invariants:
1. If an element has a column index, its tangent motions can be found in that column of the tangent space basis matrices.
2. If an element is affected by a constraint, it has a column index.
The comments in `assembly.rs` state the invariants and describe how they're enforced.
#### Automated testing
The test `engine::tests::tangent_test` builds a simple assembly with a known tangent space, runs the realization routine, and checks the returned tangent space against a hand-computed basis.
#### Limitations
The method `ConfigSubspace::symmetric_kernel` approximates the kernel by taking all the eigenspaces whose eigenvalues are smaller than a hard-coded threshold size. We may need a more flexible system eventually.
### Deformation
#### Implementation
The main purpose of this implementation is to confirm that deformation works as we'd hoped. The code is messy, and the deformation routine has at least one numerical quirk.
For simplicity, the keyboard commands that manipulate the assembly are handled by the display, just like the keyboard commands that control the camera. Deformation happens at the beginning of the animation loop.
The function `Assembly::deform` works like this:
1. Take a list of element motions
2. Project them onto the tangent space of the solution variety
3. Sum them to get a deformation $v$ of the whole assembly
4. Step the assembly along the "mass shell" geodesic tangent to $v$
* This step stays on the solution variety to first order
5. Call `realize` to bring the assembly back onto the solution variety
#### Manual testing
To manipulate the assembly:
1. Select a sphere
2. Make sure the display has focus
3. Hold the following keys:
* **A**/**D** for $x$ translation
* **W**/**S** for $y$ translation
* **shift**+**W**/**S** for $z$ translation
#### Limitations
Because the manipulation commands are handled by the display, you can only manipulate the assembly when the display has focus.
Since our test assemblies only include spheres, we assume in `Assembly::deform` that every element is a sphere.
When the tangent space is zero, `Assembly::deform` does nothing except print "The assembly is rigid" to the console.
During a deformation, the curvature and co-curvature components of a sphere's vector representation can exhibit weird discontinuous "swaps" that don't visibly affect how the sphere is drawn. *[I'll write more about this in an issue.]*
Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/29
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2024-12-30 22:53:07 +00:00
|
|
|
// swap navigation inputs
|
2024-10-21 23:38:27 +00:00
|
|
|
yaw_right.set(roll_cw.get());
|
|
|
|
yaw_left.set(roll_ccw.get());
|
|
|
|
pitch_up.set(zoom_in.get());
|
|
|
|
pitch_down.set(zoom_out.get());
|
|
|
|
roll_cw.set(0.0);
|
|
|
|
roll_ccw.set(0.0);
|
|
|
|
zoom_in.set(0.0);
|
|
|
|
zoom_out.set(0.0);
|
Manipulate the assembly (#29)
feat: Find tangent space of solution variety, use for perturbations
### Tangent space
#### Implementation
The structure `engine::ConfigSubspace` represents a subspace of the configuration vector space $\operatorname{Hom}(\mathbb{R}^n, \mathbb{R}^5)$. It holds a basis for the subspace which is orthonormal with respect to the Euclidean inner product. The method `ConfigSubspace::symmetric_kernel` takes an endomorphism of the configuration vector space, which must be symmetric with respect to the Euclidean inner product, and returns its approximate kernel in the form of a `ConfigSubspace`.
At the end of `engine::realize_gram`, we use the computed Hessian to find the tangent space of the solution variety, and we return it alongside the realization. Since altering the constraints can change the tangent space without changing the solution, we compute the tangent space even when the guess passed to the realization routine is already a solution.
After `Assembly::realize` calls `engine::realize_gram`, it saves the returned tangent space in the assembly's `tangent` signal. The basis vectors are stored in configuration matrix format, ordered according to the elements' column indices. To help maintain consistency between the storage layout of the tangent space and the elements' column indices, we switch the column index data type from `usize` to `Option<usize>` and enforce the following invariants:
1. If an element has a column index, its tangent motions can be found in that column of the tangent space basis matrices.
2. If an element is affected by a constraint, it has a column index.
The comments in `assembly.rs` state the invariants and describe how they're enforced.
#### Automated testing
The test `engine::tests::tangent_test` builds a simple assembly with a known tangent space, runs the realization routine, and checks the returned tangent space against a hand-computed basis.
#### Limitations
The method `ConfigSubspace::symmetric_kernel` approximates the kernel by taking all the eigenspaces whose eigenvalues are smaller than a hard-coded threshold size. We may need a more flexible system eventually.
### Deformation
#### Implementation
The main purpose of this implementation is to confirm that deformation works as we'd hoped. The code is messy, and the deformation routine has at least one numerical quirk.
For simplicity, the keyboard commands that manipulate the assembly are handled by the display, just like the keyboard commands that control the camera. Deformation happens at the beginning of the animation loop.
The function `Assembly::deform` works like this:
1. Take a list of element motions
2. Project them onto the tangent space of the solution variety
3. Sum them to get a deformation $v$ of the whole assembly
4. Step the assembly along the "mass shell" geodesic tangent to $v$
* This step stays on the solution variety to first order
5. Call `realize` to bring the assembly back onto the solution variety
#### Manual testing
To manipulate the assembly:
1. Select a sphere
2. Make sure the display has focus
3. Hold the following keys:
* **A**/**D** for $x$ translation
* **W**/**S** for $y$ translation
* **shift**+**W**/**S** for $z$ translation
#### Limitations
Because the manipulation commands are handled by the display, you can only manipulate the assembly when the display has focus.
Since our test assemblies only include spheres, we assume in `Assembly::deform` that every element is a sphere.
When the tangent space is zero, `Assembly::deform` does nothing except print "The assembly is rigid" to the console.
During a deformation, the curvature and co-curvature components of a sphere's vector representation can exhibit weird discontinuous "swaps" that don't visibly affect how the sphere is drawn. *[I'll write more about this in an issue.]*
Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/29
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2024-12-30 22:53:07 +00:00
|
|
|
|
|
|
|
// swap manipulation inputs
|
|
|
|
translate_pos_y.set(translate_neg_z.get());
|
|
|
|
translate_neg_y.set(translate_pos_z.get());
|
|
|
|
translate_pos_z.set(0.0);
|
|
|
|
translate_neg_z.set(0.0);
|
2024-10-21 23:38:27 +00:00
|
|
|
} else {
|
Manipulate the assembly (#29)
feat: Find tangent space of solution variety, use for perturbations
### Tangent space
#### Implementation
The structure `engine::ConfigSubspace` represents a subspace of the configuration vector space $\operatorname{Hom}(\mathbb{R}^n, \mathbb{R}^5)$. It holds a basis for the subspace which is orthonormal with respect to the Euclidean inner product. The method `ConfigSubspace::symmetric_kernel` takes an endomorphism of the configuration vector space, which must be symmetric with respect to the Euclidean inner product, and returns its approximate kernel in the form of a `ConfigSubspace`.
At the end of `engine::realize_gram`, we use the computed Hessian to find the tangent space of the solution variety, and we return it alongside the realization. Since altering the constraints can change the tangent space without changing the solution, we compute the tangent space even when the guess passed to the realization routine is already a solution.
After `Assembly::realize` calls `engine::realize_gram`, it saves the returned tangent space in the assembly's `tangent` signal. The basis vectors are stored in configuration matrix format, ordered according to the elements' column indices. To help maintain consistency between the storage layout of the tangent space and the elements' column indices, we switch the column index data type from `usize` to `Option<usize>` and enforce the following invariants:
1. If an element has a column index, its tangent motions can be found in that column of the tangent space basis matrices.
2. If an element is affected by a constraint, it has a column index.
The comments in `assembly.rs` state the invariants and describe how they're enforced.
#### Automated testing
The test `engine::tests::tangent_test` builds a simple assembly with a known tangent space, runs the realization routine, and checks the returned tangent space against a hand-computed basis.
#### Limitations
The method `ConfigSubspace::symmetric_kernel` approximates the kernel by taking all the eigenspaces whose eigenvalues are smaller than a hard-coded threshold size. We may need a more flexible system eventually.
### Deformation
#### Implementation
The main purpose of this implementation is to confirm that deformation works as we'd hoped. The code is messy, and the deformation routine has at least one numerical quirk.
For simplicity, the keyboard commands that manipulate the assembly are handled by the display, just like the keyboard commands that control the camera. Deformation happens at the beginning of the animation loop.
The function `Assembly::deform` works like this:
1. Take a list of element motions
2. Project them onto the tangent space of the solution variety
3. Sum them to get a deformation $v$ of the whole assembly
4. Step the assembly along the "mass shell" geodesic tangent to $v$
* This step stays on the solution variety to first order
5. Call `realize` to bring the assembly back onto the solution variety
#### Manual testing
To manipulate the assembly:
1. Select a sphere
2. Make sure the display has focus
3. Hold the following keys:
* **A**/**D** for $x$ translation
* **W**/**S** for $y$ translation
* **shift**+**W**/**S** for $z$ translation
#### Limitations
Because the manipulation commands are handled by the display, you can only manipulate the assembly when the display has focus.
Since our test assemblies only include spheres, we assume in `Assembly::deform` that every element is a sphere.
When the tangent space is zero, `Assembly::deform` does nothing except print "The assembly is rigid" to the console.
During a deformation, the curvature and co-curvature components of a sphere's vector representation can exhibit weird discontinuous "swaps" that don't visibly affect how the sphere is drawn. *[I'll write more about this in an issue.]*
Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Reviewed-on: https://code.studioinfinity.org/glen/dyna3/pulls/29
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
2024-12-30 22:53:07 +00:00
|
|
|
set_nav_signal(&event, 0.0);
|
|
|
|
set_manip_signal(&event, 0.0);
|
2024-10-21 23:38:27 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
on:blur=move |_| {
|
|
|
|
pitch_up.set(0.0);
|
|
|
|
pitch_down.set(0.0);
|
|
|
|
yaw_right.set(0.0);
|
|
|
|
yaw_left.set(0.0);
|
|
|
|
roll_ccw.set(0.0);
|
|
|
|
roll_cw.set(0.0);
|
2024-11-27 05:02:06 +00:00
|
|
|
},
|
|
|
|
on:click=move |event: MouseEvent| {
|
|
|
|
// find the nearest element along the pointer direction
|
2025-05-01 19:25:13 +00:00
|
|
|
let (dir, pixel_size) = event_dir(&event);
|
2024-11-27 05:02:06 +00:00
|
|
|
console::log_1(&JsValue::from(dir.to_string()));
|
2025-05-06 19:17:30 +00:00
|
|
|
let mut clicked: Option<(Rc<dyn Element>, f64)> = None;
|
2025-06-02 15:56:06 +00:00
|
|
|
let tangible_elts = state.assembly.elements
|
|
|
|
.get_clone_untracked()
|
|
|
|
.into_iter()
|
|
|
|
.filter(|elt| !elt.ghost().get());
|
|
|
|
for elt in tangible_elts {
|
2025-05-01 19:25:13 +00:00
|
|
|
match assembly_to_world.with(|asm_to_world| elt.cast(dir, asm_to_world, pixel_size)) {
|
2024-11-27 05:02:06 +00:00
|
|
|
Some(depth) => match clicked {
|
|
|
|
Some((_, best_depth)) => {
|
|
|
|
if depth < best_depth {
|
2025-05-06 19:17:30 +00:00
|
|
|
clicked = Some((elt, depth))
|
2024-11-27 05:02:06 +00:00
|
|
|
}
|
|
|
|
},
|
2025-05-06 19:17:30 +00:00
|
|
|
None => clicked = Some((elt, depth))
|
2024-11-27 05:02:06 +00:00
|
|
|
}
|
|
|
|
None => ()
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
// if we clicked something, select it
|
|
|
|
match clicked {
|
2025-05-06 19:17:30 +00:00
|
|
|
Some((elt, _)) => state.select(&elt, event.shift_key()),
|
2024-11-27 05:02:06 +00:00
|
|
|
None => state.selection.update(|sel| sel.clear())
|
|
|
|
};
|
2024-10-21 23:38:27 +00:00
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|