Introduce ghost mode for elements (#85)

Allows any element to be put in "ghost mode," decreasing its opacity and making it insensitive to click-to-select. Ghost mode is toggled using a checkbox in the outline view.

Co-authored-by: Aaron Fenyes <aaron.fenyes@fareycircles.ooo>
Reviewed-on: StudioInfinity/dyna3#85
Co-authored-by: Vectornaut <vectornaut@nobody@nowhere.net>
Co-committed-by: Vectornaut <vectornaut@nobody@nowhere.net>
This commit is contained in:
Vectornaut 2025-06-02 15:56:06 +00:00 committed by Glen Whitney
parent 2adf4669f4
commit a671a8273a
7 changed files with 77 additions and 31 deletions

View file

@ -90,6 +90,10 @@ summary > div, .regulator {
padding-right: 8px; padding-right: 8px;
} }
.element > input {
margin-left: 8px;
}
.element-switch { .element-switch {
width: 18px; width: 18px;
padding-left: 2px; padding-left: 2px;

View file

@ -101,6 +101,7 @@ pub trait Element: Serial + ProblemPoser + DisplayItem {
fn id(&self) -> &String; fn id(&self) -> &String;
fn label(&self) -> &String; fn label(&self) -> &String;
fn representation(&self) -> Signal<DVector<f64>>; fn representation(&self) -> Signal<DVector<f64>>;
fn ghost(&self) -> Signal<bool>;
// the regulators the element is subject to. the assembly that owns the // the regulators the element is subject to. the assembly that owns the
// element is responsible for keeping this set up to date // element is responsible for keeping this set up to date
@ -154,6 +155,7 @@ pub struct Sphere {
pub label: String, pub label: String,
pub color: ElementColor, pub color: ElementColor,
pub representation: Signal<DVector<f64>>, pub representation: Signal<DVector<f64>>,
pub ghost: Signal<bool>,
pub regulators: Signal<BTreeSet<Rc<dyn Regulator>>>, pub regulators: Signal<BTreeSet<Rc<dyn Regulator>>>,
serial: u64, serial: u64,
column_index: Cell<Option<usize>> column_index: Cell<Option<usize>>
@ -173,6 +175,7 @@ impl Sphere {
label: label, label: label,
color: color, color: color,
representation: create_signal(representation), representation: create_signal(representation),
ghost: create_signal(false),
regulators: create_signal(BTreeSet::new()), regulators: create_signal(BTreeSet::new()),
serial: Self::next_serial(), serial: Self::next_serial(),
column_index: None.into() column_index: None.into()
@ -210,6 +213,10 @@ impl Element for Sphere {
self.representation self.representation
} }
fn ghost(&self) -> Signal<bool> {
self.ghost
}
fn regulators(&self) -> Signal<BTreeSet<Rc<dyn Regulator>>> { fn regulators(&self) -> Signal<BTreeSet<Rc<dyn Regulator>>> {
self.regulators self.regulators
} }
@ -244,6 +251,7 @@ pub struct Point {
pub label: String, pub label: String,
pub color: ElementColor, pub color: ElementColor,
pub representation: Signal<DVector<f64>>, pub representation: Signal<DVector<f64>>,
pub ghost: Signal<bool>,
pub regulators: Signal<BTreeSet<Rc<dyn Regulator>>>, pub regulators: Signal<BTreeSet<Rc<dyn Regulator>>>,
serial: u64, serial: u64,
column_index: Cell<Option<usize>> column_index: Cell<Option<usize>>
@ -263,6 +271,7 @@ impl Point {
label, label,
color, color,
representation: create_signal(representation), representation: create_signal(representation),
ghost: create_signal(false),
regulators: create_signal(BTreeSet::new()), regulators: create_signal(BTreeSet::new()),
serial: Self::next_serial(), serial: Self::next_serial(),
column_index: None.into() column_index: None.into()
@ -296,6 +305,10 @@ impl Element for Point {
self.representation self.representation
} }
fn ghost(&self) -> Signal<bool> {
self.ghost
}
fn regulators(&self) -> Signal<BTreeSet<Rc<dyn Regulator>>> { fn regulators(&self) -> Signal<BTreeSet<Rc<dyn Regulator>>> {
self.regulators self.regulators
} }

View file

@ -20,11 +20,23 @@ use crate::{
assembly::{Element, ElementColor, ElementMotion, Point, Sphere} assembly::{Element, ElementColor, ElementMotion, Point, Sphere}
}; };
// --- 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
}
// --- scene data --- // --- scene data ---
struct SceneSpheres { struct SceneSpheres {
representations: Vec<DVector<f64>>, representations: Vec<DVector<f64>>,
colors: Vec<ElementColor>, colors_with_opacity: Vec<ColorWithOpacity>,
highlights: Vec<f32> highlights: Vec<f32>
} }
@ -32,7 +44,7 @@ impl SceneSpheres {
fn new() -> SceneSpheres{ fn new() -> SceneSpheres{
SceneSpheres { SceneSpheres {
representations: Vec::new(), representations: Vec::new(),
colors: Vec::new(), colors_with_opacity: Vec::new(),
highlights: Vec::new() highlights: Vec::new()
} }
} }
@ -41,16 +53,16 @@ impl SceneSpheres {
self.representations.len().try_into().expect("Number of spheres must fit in a 32-bit integer") self.representations.len().try_into().expect("Number of spheres must fit in a 32-bit integer")
} }
fn push(&mut self, representation: DVector<f64>, color: ElementColor, highlight: f32) { fn push(&mut self, representation: DVector<f64>, color: ElementColor, opacity: f32, highlight: f32) {
self.representations.push(representation); self.representations.push(representation);
self.colors.push(color); self.colors_with_opacity.push(combine_channels(color, opacity));
self.highlights.push(highlight); self.highlights.push(highlight);
} }
} }
struct ScenePoints { struct ScenePoints {
representations: Vec<DVector<f64>>, representations: Vec<DVector<f64>>,
colors: Vec<ElementColor>, colors_with_opacity: Vec<ColorWithOpacity>,
highlights: Vec<f32>, highlights: Vec<f32>,
selections: Vec<f32> selections: Vec<f32>
} }
@ -59,15 +71,15 @@ impl ScenePoints {
fn new() -> ScenePoints { fn new() -> ScenePoints {
ScenePoints { ScenePoints {
representations: Vec::new(), representations: Vec::new(),
colors: Vec::new(), colors_with_opacity: Vec::new(),
highlights: Vec::new(), highlights: Vec::new(),
selections: Vec::new() selections: Vec::new()
} }
} }
fn push(&mut self, representation: DVector<f64>, color: ElementColor, highlight: f32, selected: bool) { fn push(&mut self, representation: DVector<f64>, color: ElementColor, opacity: f32, highlight: f32, selected: bool) {
self.representations.push(representation); self.representations.push(representation);
self.colors.push(color); self.colors_with_opacity.push(combine_channels(color, opacity));
self.highlights.push(highlight); self.highlights.push(highlight);
self.selections.push(if selected { 1.0 } else { 0.0 }); self.selections.push(if selected { 1.0 } else { 0.0 });
} }
@ -98,11 +110,16 @@ pub trait DisplayItem {
impl DisplayItem for Sphere { impl DisplayItem for Sphere {
fn show(&self, scene: &mut Scene, selected: bool) { fn show(&self, scene: &mut Scene, selected: bool) {
const HIGHLIGHT: f32 = 0.2; /* SCAFFOLDING */ /* SCAFFOLDING */
const DEFAULT_OPACITY: f32 = 0.5;
const GHOST_OPACITY: f32 = 0.2;
const HIGHLIGHT: f32 = 0.2;
let representation = self.representation.get_clone_untracked(); let representation = self.representation.get_clone_untracked();
let color = if selected { self.color.map(|channel| 0.2 + 0.8*channel) } else { self.color }; let color = if selected { self.color.map(|channel| 0.2 + 0.8*channel) } else { self.color };
let opacity = if self.ghost.get() { GHOST_OPACITY } else { DEFAULT_OPACITY };
let highlight = if selected { 1.0 } else { HIGHLIGHT }; let highlight = if selected { 1.0 } else { HIGHLIGHT };
scene.spheres.push(representation, color, highlight); scene.spheres.push(representation, color, opacity, highlight);
} }
// this method should be kept synchronized with `sphere_cast` in // this method should be kept synchronized with `sphere_cast` in
@ -148,11 +165,15 @@ impl DisplayItem for Sphere {
impl DisplayItem for Point { impl DisplayItem for Point {
fn show(&self, scene: &mut Scene, selected: bool) { fn show(&self, scene: &mut Scene, selected: bool) {
const HIGHLIGHT: f32 = 0.5; /* SCAFFOLDING */ /* SCAFFOLDING */
const GHOST_OPACITY: f32 = 0.4;
const HIGHLIGHT: f32 = 0.5;
let representation = self.representation.get_clone_untracked(); let representation = self.representation.get_clone_untracked();
let color = if selected { self.color.map(|channel| 0.2 + 0.8*channel) } else { self.color }; let color = if selected { self.color.map(|channel| 0.2 + 0.8*channel) } else { self.color };
let opacity = if self.ghost.get() { GHOST_OPACITY } else { 1.0 };
let highlight = if selected { 1.0 } else { HIGHLIGHT }; let highlight = if selected { 1.0 } else { HIGHLIGHT };
scene.points.push(representation, color, highlight, selected); scene.points.push(representation, color, opacity, highlight, selected);
} }
/* SCAFFOLDING */ /* SCAFFOLDING */
@ -365,6 +386,7 @@ pub fn Display() -> View {
state.assembly.elements.with(|elts| { state.assembly.elements.with(|elts| {
for elt in elts { for elt in elts {
elt.representation().track(); elt.representation().track();
elt.ghost().track();
} }
}); });
state.selection.track(); state.selection.track();
@ -395,7 +417,6 @@ pub fn Display() -> View {
const SHRINKING_SPEED: f64 = 0.15; // in length units per second const SHRINKING_SPEED: f64 = 0.15; // in length units per second
// display parameters // display parameters
const OPACITY: f32 = 0.5; /* SCAFFOLDING */
const LAYER_THRESHOLD: i32 = 0; /* DEBUG */ const LAYER_THRESHOLD: i32 = 0; /* DEBUG */
const DEBUG_MODE: i32 = 0; /* DEBUG */ const DEBUG_MODE: i32 = 0; /* DEBUG */
@ -469,7 +490,6 @@ pub fn Display() -> View {
); );
let resolution_loc = ctx.get_uniform_location(&sphere_program, "resolution"); let resolution_loc = ctx.get_uniform_location(&sphere_program, "resolution");
let shortdim_loc = ctx.get_uniform_location(&sphere_program, "shortdim"); let shortdim_loc = ctx.get_uniform_location(&sphere_program, "shortdim");
let opacity_loc = ctx.get_uniform_location(&sphere_program, "opacity");
let layer_threshold_loc = ctx.get_uniform_location(&sphere_program, "layer_threshold"); let layer_threshold_loc = ctx.get_uniform_location(&sphere_program, "layer_threshold");
let debug_mode_loc = ctx.get_uniform_location(&sphere_program, "debug_mode"); let debug_mode_loc = ctx.get_uniform_location(&sphere_program, "debug_mode");
@ -654,9 +674,9 @@ pub fn Display() -> View {
sphere_lt_locs[n].as_ref(), sphere_lt_locs[n].as_ref(),
v.rows(3, 2).as_slice() v.rows(3, 2).as_slice()
); );
ctx.uniform3fv_with_f32_array( ctx.uniform4fv_with_f32_array(
sphere_color_locs[n].as_ref(), sphere_color_locs[n].as_ref(),
&scene.spheres.colors[n] &scene.spheres.colors_with_opacity[n]
); );
ctx.uniform1f( ctx.uniform1f(
sphere_highlight_locs[n].as_ref(), sphere_highlight_locs[n].as_ref(),
@ -665,7 +685,6 @@ pub fn Display() -> View {
} }
// pass the display parameters // pass the display parameters
ctx.uniform1f(opacity_loc.as_ref(), OPACITY);
ctx.uniform1i(layer_threshold_loc.as_ref(), LAYER_THRESHOLD); ctx.uniform1i(layer_threshold_loc.as_ref(), LAYER_THRESHOLD);
ctx.uniform1i(debug_mode_loc.as_ref(), DEBUG_MODE); ctx.uniform1i(debug_mode_loc.as_ref(), DEBUG_MODE);
@ -703,7 +722,7 @@ pub fn Display() -> View {
// bind them to the corresponding attributes in the vertex // bind them to the corresponding attributes in the vertex
// shader // shader
bind_new_buffer_to_attribute(&ctx, point_position_attr, SPACE_DIM as i32, point_positions.as_slice()); bind_new_buffer_to_attribute(&ctx, point_position_attr, SPACE_DIM as i32, point_positions.as_slice());
bind_new_buffer_to_attribute(&ctx, point_color_attr, COLOR_SIZE as i32, scene.points.colors.concat().as_slice()); bind_new_buffer_to_attribute(&ctx, point_color_attr, (COLOR_SIZE + 1) as i32, scene.points.colors_with_opacity.concat().as_slice());
bind_new_buffer_to_attribute(&ctx, point_highlight_attr, 1 as i32, scene.points.highlights.as_slice()); bind_new_buffer_to_attribute(&ctx, point_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()); bind_new_buffer_to_attribute(&ctx, point_selection_attr, 1 as i32, scene.points.selections.as_slice());
@ -851,7 +870,11 @@ pub fn Display() -> View {
let (dir, pixel_size) = event_dir(&event); let (dir, pixel_size) = event_dir(&event);
console::log_1(&JsValue::from(dir.to_string())); console::log_1(&JsValue::from(dir.to_string()));
let mut clicked: Option<(Rc<dyn Element>, f64)> = None; let mut clicked: Option<(Rc<dyn Element>, f64)> = None;
for elt in state.assembly.elements.get_clone_untracked() { let tangible_elts = state.assembly.elements
.get_clone_untracked()
.into_iter()
.filter(|elt| !elt.ghost().get());
for elt in tangible_elts {
match assembly_to_world.with(|asm_to_world| elt.cast(dir, asm_to_world, pixel_size)) { match assembly_to_world.with(|asm_to_world| elt.cast(dir, asm_to_world, pixel_size)) {
Some(depth) => match clicked { Some(depth) => match clicked {
Some((_, best_depth)) => { Some((_, best_depth)) => {

View file

@ -202,7 +202,11 @@ fn ElementOutlineItem(element: Rc<dyn Element>) -> View {
) { ) {
div(class="element-label") { (label) } div(class="element-label") { (label) }
div(class="element-representation") { (rep_components) } div(class="element-representation") { (rep_components) }
div(class="status") input(
r#type="checkbox",
bind:checked=element.ghost(),
on:click=|event: MouseEvent| event.stop_propagation()
)
} }
} }
ul(class="regulators") { ul(class="regulators") {

View file

@ -2,7 +2,7 @@
precision highp float; precision highp float;
in vec3 point_color; in vec4 point_color;
in float point_highlight; in float point_highlight;
in float total_radius; in float total_radius;
@ -13,6 +13,7 @@ void main() {
const float POINT_RADIUS = 4.; const float POINT_RADIUS = 4.;
float border = smoothstep(POINT_RADIUS - 1., POINT_RADIUS, r); float border = smoothstep(POINT_RADIUS - 1., POINT_RADIUS, r);
vec3 color = mix(point_color, vec3(1.), border * point_highlight); float disk = 1. - smoothstep(total_radius - 1., total_radius, r);
outColor = vec4(color, 1. - smoothstep(total_radius - 1., total_radius, r)); vec4 color = mix(point_color, vec4(1.), border * point_highlight);
outColor = vec4(vec3(1.), disk) * color;
} }

View file

@ -1,11 +1,11 @@
#version 300 es #version 300 es
in vec4 position; in vec4 position;
in vec3 color; in vec4 color;
in float highlight; in float highlight;
in float selected; in float selected;
out vec3 point_color; out vec4 point_color;
out float point_highlight; out float point_highlight;
out float total_radius; out float total_radius;

View file

@ -17,7 +17,7 @@ struct vecInv {
const int SPHERE_MAX = 200; const int SPHERE_MAX = 200;
uniform int sphere_cnt; uniform int sphere_cnt;
uniform vecInv sphere_list[SPHERE_MAX]; uniform vecInv sphere_list[SPHERE_MAX];
uniform vec3 color_list[SPHERE_MAX]; uniform vec4 color_list[SPHERE_MAX];
uniform float highlight_list[SPHERE_MAX]; uniform float highlight_list[SPHERE_MAX];
// view // view
@ -25,7 +25,6 @@ uniform vec2 resolution;
uniform float shortdim; uniform float shortdim;
// controls // controls
uniform float opacity;
uniform int layer_threshold; uniform int layer_threshold;
uniform bool debug_mode; uniform bool debug_mode;
@ -69,7 +68,7 @@ struct Fragment {
vec4 color; vec4 color;
}; };
Fragment sphere_shading(vecInv v, vec3 pt, vec3 base_color) { Fragment sphere_shading(vecInv v, vec3 pt, vec4 base_color) {
// the expression for normal needs to be checked. it's supposed to give the // the expression for normal needs to be checked. it's supposed to give the
// negative gradient of the lorentz product between the impact point vector // negative gradient of the lorentz product between the impact point vector
// and the sphere vector with respect to the coordinates of the impact // and the sphere vector with respect to the coordinates of the impact
@ -79,7 +78,7 @@ Fragment sphere_shading(vecInv v, vec3 pt, vec3 base_color) {
float incidence = dot(normal, light_dir); float incidence = dot(normal, light_dir);
float illum = mix(0.4, 1.0, max(incidence, 0.0)); float illum = mix(0.4, 1.0, max(incidence, 0.0));
return Fragment(pt, normal, vec4(illum * base_color, opacity)); return Fragment(pt, normal, vec4(illum * base_color.rgb, base_color.a));
} }
float intersection_dist(Fragment a, Fragment b) { float intersection_dist(Fragment a, Fragment b) {
@ -192,10 +191,11 @@ void main() {
vec3 color = vec3(0.); vec3 color = vec3(0.);
int layer = layer_cnt - 1; int layer = layer_cnt - 1;
TaggedDepth hit = top_hits[layer]; TaggedDepth hit = top_hits[layer];
vec4 sphere_color = color_list[hit.id];
Fragment frag_next = sphere_shading( Fragment frag_next = sphere_shading(
sphere_list[hit.id], sphere_list[hit.id],
hit.depth * dir, hit.depth * dir,
hit.dimming * color_list[hit.id] vec4(hit.dimming * sphere_color.rgb, sphere_color.a)
); );
float highlight_next = highlight_list[hit.id]; float highlight_next = highlight_list[hit.id];
--layer; --layer;
@ -206,10 +206,11 @@ void main() {
// shade the next fragment // shade the next fragment
hit = top_hits[layer]; hit = top_hits[layer];
sphere_color = color_list[hit.id];
frag_next = sphere_shading( frag_next = sphere_shading(
sphere_list[hit.id], sphere_list[hit.id],
hit.depth * dir, hit.depth * dir,
hit.dimming * color_list[hit.id] vec4(hit.dimming * sphere_color.rgb, sphere_color.a)
); );
highlight_next = highlight_list[hit.id]; highlight_next = highlight_list[hit.id];