From 523cc275230c5a9c10b79c0c6318d6227d5473cc Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Fri, 6 Jun 2025 21:19:03 -0700 Subject: [PATCH] Improve the spreadsheet visualization details for VectorData --- .../spreadsheet_message_handler.rs | 63 +++++++++++++------ node-graph/gcore/src/graphic_element.rs | 7 +++ node-graph/gcore/src/vector/style.rs | 37 +++++++++++ node-graph/gcore/src/vector/vector_data.rs | 36 +++++++---- 4 files changed, 110 insertions(+), 33 deletions(-) diff --git a/editor/src/messages/portfolio/spreadsheet/spreadsheet_message_handler.rs b/editor/src/messages/portfolio/spreadsheet/spreadsheet_message_handler.rs index b087ed4f1..bd68bc733 100644 --- a/editor/src/messages/portfolio/spreadsheet/spreadsheet_message_handler.rs +++ b/editor/src/messages/portfolio/spreadsheet/spreadsheet_message_handler.rs @@ -182,19 +182,42 @@ impl InstanceLayout for VectorData { format!("Vector Data (points={}, segments={})", self.point_domain.ids().len(), self.segment_domain.ids().len()) } fn compute_layout(&self, data: &mut LayoutData) -> Vec { - let mut rows = Vec::new(); + let colinear = self.colinear_manipulators.iter().map(|[a, b]| format!("[{a} / {b}]")).collect::>().join(", "); + let colinear = if colinear.is_empty() { "None" } else { &colinear }; + let style = vec![ + TextLabel::new(format!( + "{}\n\nColinear Handle IDs: {}\n\nUpstream Graphic Group Table: {}", + self.style, + colinear, + if self.upstream_graphic_group.is_some() { "Yes" } else { "No" } + )) + .multiline(true) + .widget_holder(), + ]; + + let domain_entries = [VectorDataDomain::Points, VectorDataDomain::Segments, VectorDataDomain::Regions] + .into_iter() + .map(|domain| { + RadioEntryData::new(format!("{domain:?}")) + .label(format!("{domain:?}")) + .on_update(move |_| SpreadsheetMessage::ViewVectorDataDomain { domain }.into()) + }) + .collect(); + let domain = vec![RadioInput::new(domain_entries).selected_index(Some(data.vector_data_domain as u32)).widget_holder()]; + + let mut table_rows = Vec::new(); match data.vector_data_domain { VectorDataDomain::Points => { - rows.push(column_headings(&["", "position"])); - rows.extend( + table_rows.push(column_headings(&["", "position"])); + table_rows.extend( self.point_domain .iter() .map(|(id, position)| vec![TextLabel::new(format!("{}", id.inner())).widget_holder(), TextLabel::new(format!("{}", position)).widget_holder()]), ); } VectorDataDomain::Segments => { - rows.push(column_headings(&["", "start_index", "end_index", "handles"])); - rows.extend(self.segment_domain.iter().map(|(id, start, end, handles)| { + table_rows.push(column_headings(&["", "start_index", "end_index", "handles"])); + table_rows.extend(self.segment_domain.iter().map(|(id, start, end, handles)| { vec![ TextLabel::new(format!("{}", id.inner())).widget_holder(), TextLabel::new(format!("{}", start)).widget_holder(), @@ -204,8 +227,8 @@ impl InstanceLayout for VectorData { })); } VectorDataDomain::Regions => { - rows.push(column_headings(&["", "segment_range", "fill"])); - rows.extend(self.region_domain.iter().map(|(id, segment_range, fill)| { + table_rows.push(column_headings(&["", "segment_range", "fill"])); + table_rows.extend(self.region_domain.iter().map(|(id, segment_range, fill)| { vec![ TextLabel::new(format!("{}", id.inner())).widget_holder(), TextLabel::new(format!("{:?}", segment_range)).widget_holder(), @@ -215,17 +238,7 @@ impl InstanceLayout for VectorData { } } - let entries = [VectorDataDomain::Points, VectorDataDomain::Segments, VectorDataDomain::Regions] - .into_iter() - .map(|domain| { - RadioEntryData::new(format!("{domain:?}")) - .label(format!("{domain:?}")) - .on_update(move |_| SpreadsheetMessage::ViewVectorDataDomain { domain }.into()) - }) - .collect(); - - let domain = vec![RadioInput::new(entries).selected_index(Some(data.vector_data_domain as u32)).widget_holder()]; - vec![LayoutGroup::Row { widgets: domain }, LayoutGroup::Table { rows }] + vec![LayoutGroup::Row { widgets: style }, LayoutGroup::Row { widgets: domain }, LayoutGroup::Table { rows: table_rows }] } } @@ -278,13 +291,23 @@ impl InstanceLayout for Instances { .instance_ref_iter() .enumerate() .map(|(index, instance)| { + let (scale, angle, translation) = instance.transform.to_scale_angle_translation(); + let rotation = if angle == -0. { 0. } else { angle.to_degrees() }; + let round = |x: f64| (x * 1e3).round() / 1e3; vec![ TextLabel::new(format!("{}", index)).widget_holder(), TextButton::new(instance.instance.identifier()) .on_update(move |_| SpreadsheetMessage::PushToInstancePath { index }.into()) .widget_holder(), - TextLabel::new(format!("{}", instance.transform)).widget_holder(), - TextLabel::new(format!("{:?}", instance.alpha_blending)).widget_holder(), + TextLabel::new(format!( + "Location: ({} px, {} px) — Rotation: {rotation:2}° — Scale: ({}x, {}x)", + round(translation.x), + round(translation.y), + round(scale.x), + round(scale.y) + )) + .widget_holder(), + TextLabel::new(format!("{}", instance.alpha_blending)).widget_holder(), TextLabel::new(instance.source_node_id.map_or_else(|| "-".to_string(), |id| format!("{}", id.0))).widget_holder(), ] }) diff --git a/node-graph/gcore/src/graphic_element.rs b/node-graph/gcore/src/graphic_element.rs index f4eab5633..b05109acc 100644 --- a/node-graph/gcore/src/graphic_element.rs +++ b/node-graph/gcore/src/graphic_element.rs @@ -29,6 +29,13 @@ impl core::hash::Hash for AlphaBlending { self.blend_mode.hash(state); } } +impl std::fmt::Display for AlphaBlending { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let round = |x: f32| (x * 1e3).round() / 1e3; + write!(f, "Opacity: {}% — Blend Mode: {}", round(self.opacity * 100.), self.blend_mode) + } +} + impl AlphaBlending { pub const fn new() -> Self { Self { diff --git a/node-graph/gcore/src/vector/style.rs b/node-graph/gcore/src/vector/style.rs index ffb307749..2f0676b46 100644 --- a/node-graph/gcore/src/vector/style.rs +++ b/node-graph/gcore/src/vector/style.rs @@ -160,6 +160,20 @@ impl core::hash::Hash for Gradient { } } +impl std::fmt::Display for Gradient { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let round = |x: f64| (x * 1e3).round() / 1e3; + let stops = self + .stops + .0 + .iter() + .map(|(position, color)| format!("[{}%: #{}]", round(position * 100.), color.to_rgba_hex_srgb())) + .collect::>() + .join(", "); + write!(f, "{} Gradient: {stops}", self.gradient_type) + } +} + impl Gradient { /// Constructs a new gradient with the colors at 0 and 1 specified. pub fn new(start: DVec2, start_color: Color, end: DVec2, end_color: Color, transform: DAffine2, gradient_type: GradientType) -> Self { @@ -308,6 +322,16 @@ pub enum Fill { Gradient(Gradient), } +impl std::fmt::Display for Fill { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::None => write!(f, "None"), + Self::Solid(color) => write!(f, "#{} (Alpha: {}%)", color.to_rgb_hex_srgb(), color.a() * 100.), + Self::Gradient(gradient) => write!(f, "{}", gradient), + } + } +} + impl Fill { /// Construct a new [Fill::Solid] from a [Color]. pub fn solid(color: Color) -> Self { @@ -752,6 +776,19 @@ impl core::hash::Hash for PathStyle { } } +impl std::fmt::Display for PathStyle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let fill = &self.fill; + + let stroke = match &self.stroke { + Some(stroke) => format!("#{} (Weight: {} px)", stroke.color.map_or("None".to_string(), |c| c.to_rgba_hex_srgb()), stroke.weight), + None => "None".to_string(), + }; + + write!(f, "Fill: {fill}\nStroke: {stroke}") + } +} + impl PathStyle { pub const fn new(stroke: Option, fill: Fill) -> Self { Self { stroke, fill } diff --git a/node-graph/gcore/src/vector/vector_data.rs b/node-graph/gcore/src/vector/vector_data.rs index fd759f5b9..9228ccdb8 100644 --- a/node-graph/gcore/src/vector/vector_data.rs +++ b/node-graph/gcore/src/vector/vector_data.rs @@ -91,6 +91,19 @@ pub struct VectorData { pub upstream_graphic_group: Option, } +impl Default for VectorData { + fn default() -> Self { + Self { + style: PathStyle::new(Some(Stroke::new(Some(Color::BLACK), 0.)), super::style::Fill::None), + colinear_manipulators: Vec::new(), + point_domain: PointDomain::new(), + segment_domain: SegmentDomain::new(), + region_domain: RegionDomain::new(), + upstream_graphic_group: None, + } + } +} + impl core::hash::Hash for VectorData { fn hash(&self, state: &mut H) { self.point_domain.hash(state); @@ -450,19 +463,6 @@ impl VectorData { } } -impl Default for VectorData { - fn default() -> Self { - Self { - style: PathStyle::new(Some(Stroke::new(Some(Color::BLACK), 0.)), super::style::Fill::None), - colinear_manipulators: Vec::new(), - point_domain: PointDomain::new(), - segment_domain: SegmentDomain::new(), - region_domain: RegionDomain::new(), - upstream_graphic_group: None, - } - } -} - /// A selectable part of a curve, either an anchor (start or end of a bézier) or a handle (doesn't necessarily go through the bézier but influences curvature). #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, DynAny)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -569,6 +569,16 @@ pub struct HandleId { pub segment: SegmentId, } +impl std::fmt::Display for HandleId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.ty { + // I haven't checked if "out" and "in" are reversed, or are accurate translations of the "primary" and "end" terms used in the `HandleType` enum, so this naming is an assumption. + HandleType::Primary => write!(f, "{} out", self.segment.inner()), + HandleType::End => write!(f, "{} in", self.segment.inner()), + } + } +} + impl HandleId { /// Construct a handle for the first handle on a cubic bézier or the only handle on a quadratic bézier. #[must_use]