Add Table<Color> as a graphical type (#3033)

* Reduce code duplication in bounding box impls on Table

* Working Table<Color> rendering in the graph

* Implement color and fix other rendering with Vello and polish
This commit is contained in:
Keavon Chambers
2025-08-10 01:34:33 -07:00
committed by GitHub
parent 81abfe147a
commit 2f4aef34e5
24 changed files with 462 additions and 198 deletions

View File

@@ -8,18 +8,17 @@ use std::borrow::Cow;
pub enum FrontendGraphDataType {
#[default]
General,
Number,
Artboard,
Graphic,
Raster,
Vector,
Number,
Graphic,
Artboard,
Color,
}
impl FrontendGraphDataType {
pub fn from_type(input: &Type) -> Self {
match TaggedValue::from_type_or_none(input) {
TaggedValue::Raster(_) => Self::Raster,
TaggedValue::Vector(_) => Self::Vector,
TaggedValue::U32(_)
| TaggedValue::U64(_)
| TaggedValue::F64(_)
@@ -28,8 +27,11 @@ impl FrontendGraphDataType {
| TaggedValue::VecF64(_)
| TaggedValue::VecDVec2(_)
| TaggedValue::DAffine2(_) => Self::Number,
TaggedValue::Graphic(_) => Self::Graphic,
TaggedValue::Artboard(_) => Self::Artboard,
TaggedValue::Graphic(_) => Self::Graphic,
TaggedValue::Raster(_) => Self::Raster,
TaggedValue::Vector(_) => Self::Vector,
TaggedValue::ColorTable(_) | TaggedValue::Color(_) | TaggedValue::OptionalColor(_) => Self::Color,
_ => Self::General,
}
}

View File

@@ -158,6 +158,7 @@ impl TableRowLayout for Graphic {
Self::Vector(vector) => vector.identifier(),
Self::RasterCPU(_) => "Raster (on CPU)".to_string(),
Self::RasterGPU(_) => "Raster (on GPU)".to_string(),
Self::Color(_) => "Color".to_string(),
}
}
// Don't put a breadcrumb for Graphic
@@ -170,6 +171,12 @@ impl TableRowLayout for Graphic {
Self::Vector(table) => table.layout_with_breadcrumb(data),
Self::RasterCPU(_) => label("Raster is not supported"),
Self::RasterGPU(_) => label("Raster is not supported"),
Self::Color(color) => {
let rows = vec![vec![
TextLabel::new(format!("Colors:\n{}", color.iter().map(|color| color.element.to_rgba_hex_srgb()).collect::<Vec<_>>().join("\n"))).widget_holder(),
]];
vec![LayoutGroup::Table { rows }]
}
}
}
}

View File

@@ -15,6 +15,7 @@ use graphene_std::text::FontCache;
use graphene_std::transform::Footprint;
use graphene_std::vector::Vector;
use graphene_std::vector::style::ViewMode;
use graphene_std::wasm_application_io::RenderOutputType;
use interpreted_executor::dynamic_executor::ResolvedDocumentNodeTypesDelta;
mod runtime_io;
@@ -33,7 +34,7 @@ pub struct ExecutionResponse {
execution_id: u64,
result: Result<TaggedValue, String>,
responses: VecDeque<FrontendMessage>,
transform: DAffine2,
footprint: Footprint,
vector_modify: HashMap<NodeId, Vector>,
/// The resulting value from the temporary inspected during execution
inspect_result: Option<InspectResult>,
@@ -223,7 +224,7 @@ impl NodeGraphExecutor {
fn export(&self, node_graph_output: TaggedValue, export_config: ExportConfig, responses: &mut VecDeque<Message>) -> Result<(), String> {
let TaggedValue::RenderOutput(RenderOutput {
data: graphene_std::wasm_application_io::RenderOutputType::Svg { svg, .. },
data: RenderOutputType::Svg { svg, .. },
..
}) = node_graph_output
else {
@@ -263,7 +264,7 @@ impl NodeGraphExecutor {
execution_id,
result,
responses: existing_responses,
transform,
footprint,
vector_modify,
inspect_result,
} = execution_response;
@@ -286,9 +287,9 @@ impl NodeGraphExecutor {
let execution_context = self.futures.remove(&execution_id).ok_or_else(|| "Invalid generation ID".to_string())?;
if let Some(export_config) = execution_context.export_config {
// Special handling for exporting the artwork
self.export(node_graph_output, export_config, responses)?
self.export(node_graph_output, export_config, responses)?;
} else {
self.process_node_graph_output(node_graph_output, transform, responses)?
self.process_node_graph_output(node_graph_output, footprint, responses)?;
}
responses.add_front(DeferMessage::TriggerGraphRun(execution_id, execution_context.document_id));
@@ -332,12 +333,12 @@ impl NodeGraphExecutor {
Ok(())
}
fn debug_render(render_object: impl Render, transform: DAffine2, responses: &mut VecDeque<Message>) {
fn debug_render(render_object: impl Render, footprint: Footprint, responses: &mut VecDeque<Message>) {
// Setup rendering
let mut render = SvgRender::new();
let render_params = RenderParams {
view_mode: ViewMode::Normal,
culling_bounds: None,
footprint,
thumbnail: false,
hide_artboards: false,
for_export: false,
@@ -349,24 +350,25 @@ impl NodeGraphExecutor {
render_object.render_svg(&mut render, &render_params);
// Concatenate the defs and the SVG into one string
render.wrap_with_transform(transform, None);
render.wrap_with_transform(footprint.transform, None);
let svg = render.svg.to_svg_string();
// Send to frontend
responses.add(FrontendMessage::UpdateDocumentArtwork { svg });
}
fn process_node_graph_output(&mut self, node_graph_output: TaggedValue, transform: DAffine2, responses: &mut VecDeque<Message>) -> Result<(), String> {
fn process_node_graph_output(&mut self, node_graph_output: TaggedValue, footprint: Footprint, responses: &mut VecDeque<Message>) -> Result<(), String> {
let mut render_output_metadata = RenderMetadata::default();
match node_graph_output {
TaggedValue::RenderOutput(render_output) => {
match render_output.data {
graphene_std::wasm_application_io::RenderOutputType::Svg { svg, image_data } => {
RenderOutputType::Svg { svg, image_data } => {
// Send to frontend
responses.add(FrontendMessage::UpdateImageData { image_data });
responses.add(FrontendMessage::UpdateDocumentArtwork { svg });
}
graphene_std::wasm_application_io::RenderOutputType::CanvasFrame(frame) => {
RenderOutputType::CanvasFrame(frame) => {
let matrix = format_transform_matrix(frame.transform);
let transform = if matrix.is_empty() { String::new() } else { format!(" transform=\"{matrix}\"") };
let svg = format!(
@@ -375,29 +377,23 @@ impl NodeGraphExecutor {
);
responses.add(FrontendMessage::UpdateDocumentArtwork { svg });
}
graphene_std::wasm_application_io::RenderOutputType::Texture { .. } => {}
_ => {
return Err(format!("Invalid node graph output type: {:#?}", render_output.data));
}
RenderOutputType::Texture { .. } => {}
_ => return Err(format!("Invalid node graph output type: {:#?}", render_output.data)),
}
render_output_metadata = render_output.metadata;
}
TaggedValue::Bool(render_object) => Self::debug_render(render_object, transform, responses),
TaggedValue::String(render_object) => Self::debug_render(render_object, transform, responses),
TaggedValue::F64(render_object) => Self::debug_render(render_object, transform, responses),
TaggedValue::DVec2(render_object) => Self::debug_render(render_object, transform, responses),
TaggedValue::OptionalColor(render_object) => Self::debug_render(render_object, transform, responses),
TaggedValue::Vector(render_object) => Self::debug_render(render_object, transform, responses),
TaggedValue::Graphic(render_object) => Self::debug_render(render_object, transform, responses),
TaggedValue::Raster(render_object) => Self::debug_render(render_object, transform, responses),
TaggedValue::Palette(render_object) => Self::debug_render(render_object, transform, responses),
_ => {
return Err(format!("Invalid node graph output type: {node_graph_output:#?}"));
}
TaggedValue::Bool(render_object) => Self::debug_render(render_object, footprint, responses),
TaggedValue::F64(render_object) => Self::debug_render(render_object, footprint, responses),
TaggedValue::DVec2(render_object) => Self::debug_render(render_object, footprint, responses),
TaggedValue::String(render_object) => Self::debug_render(render_object, footprint, responses),
TaggedValue::OptionalColor(render_object) => Self::debug_render(render_object, footprint, responses),
TaggedValue::Palette(render_object) => Self::debug_render(render_object, footprint, responses),
_ => return Err(format!("Invalid node graph output type: {node_graph_output:#?}")),
};
let graphene_std::renderer::RenderMetadata {
upstream_footprints: footprints,
upstream_footprints,
local_transforms,
first_element_source_id,
click_targets,
@@ -406,7 +402,7 @@ impl NodeGraphExecutor {
// Run these update state messages immediately
responses.add(DocumentMessage::UpdateUpstreamTransforms {
upstream_footprints: footprints,
upstream_footprints,
local_transforms,
first_element_source_id,
});

View File

@@ -8,11 +8,13 @@ use graph_craft::proto::GraphErrors;
use graph_craft::wasm_application_io::EditorPreferences;
use graph_craft::{ProtoNodeIdentifier, concrete};
use graphene_std::application_io::{ImageTexture, NodeGraphUpdateMessage, NodeGraphUpdateSender, RenderConfig};
use graphene_std::bounds::RenderBoundingBox;
use graphene_std::memo::IORecord;
use graphene_std::renderer::{Render, RenderParams, SvgRender};
use graphene_std::renderer::{RenderSvgSegmentList, SvgSegment};
use graphene_std::table::{Table, TableRow};
use graphene_std::text::FontCache;
use graphene_std::transform::RenderQuality;
use graphene_std::vector::Vector;
use graphene_std::vector::style::ViewMode;
use graphene_std::wasm_application_io::{RenderOutputType, WasmApplicationIo, WasmEditorApi};
@@ -202,8 +204,6 @@ impl NodeRuntime {
});
}
GraphRuntimeRequest::ExecutionRequest(ExecutionRequest { execution_id, render_config, .. }) => {
let transform = render_config.viewport.transform;
let result = self.execute_network(render_config).await;
let mut responses = VecDeque::new();
// TODO: Only process monitor nodes if the graph has changed, not when only the Footprint changes
@@ -227,7 +227,7 @@ impl NodeRuntime {
execution_id,
result,
responses,
transform,
footprint: render_config.viewport,
vector_modify: self.vector_modify.clone(),
inspect_result,
});
@@ -292,51 +292,49 @@ impl NodeRuntime {
if self.inspect_state.is_some_and(|inspect_state| monitor_node_path.last().copied() == Some(inspect_state.monitor_node)) {
continue;
}
// The monitor nodes are located within a document node, and are thus children in that network, so this gets the parent document node's ID
let Some(parent_network_node_id) = monitor_node_path.len().checked_sub(2).and_then(|index| monitor_node_path.get(index)).copied() else {
warn!("Monitor node has invalid node id");
continue;
};
// Extract the monitor node's stored `Graphic` data.
// Extract the monitor node's stored `Graphic` data
let Ok(introspected_data) = self.executor.introspect(monitor_node_path) else {
// TODO: Fix the root of the issue causing the spam of this warning (this at least temporarily disables it in release builds)
#[cfg(debug_assertions)]
warn!("Failed to introspect monitor node {}", self.executor.introspect(monitor_node_path).unwrap_err());
continue;
};
// Graphic table: thumbnail
if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, Table<Graphic>>>() {
Self::process_graphic(&mut self.thumbnail_renders, parent_network_node_id, &io.output, responses, update_thumbnails)
} else if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, Table<Artboard>>>() {
Self::process_graphic(&mut self.thumbnail_renders, parent_network_node_id, &io.output, responses, update_thumbnails)
// Insert the vector modify if we are dealing with vector data
} else if let Some(record) = introspected_data.downcast_ref::<IORecord<Context, Table<Vector>>>() {
if update_thumbnails {
Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, &io.output, responses)
}
}
// Artboard table: thumbnail
else if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, Table<Artboard>>>() {
if update_thumbnails {
Self::render_thumbnail(&mut self.thumbnail_renders, parent_network_node_id, &io.output, responses)
}
}
// Vector table: vector modifications
else if let Some(io) = introspected_data.downcast_ref::<IORecord<Context, Table<Vector>>>() {
// Insert the vector modify
let default = TableRow::default();
self.vector_modify
.insert(parent_network_node_id, record.output.iter().next().unwrap_or_else(|| default.as_ref()).element.clone());
} else {
.insert(parent_network_node_id, io.output.iter().next().unwrap_or_else(|| default.as_ref()).element.clone());
}
// Other
else {
log::warn!("Failed to downcast monitor node output {parent_network_node_id:?}");
}
}
}
// If this is `Graphic` data, regenerate click targets and thumbnails for the layers in the graph, modifying the state and updating the UI.
fn process_graphic(
thumbnail_renders: &mut HashMap<NodeId, Vec<SvgSegment>>,
parent_network_node_id: NodeId,
graphic: &impl Render,
responses: &mut VecDeque<FrontendMessage>,
update_thumbnails: bool,
) {
// RENDER THUMBNAIL
if !update_thumbnails {
return;
}
/// If this is `Graphic` data, regenerate click targets and thumbnails for the layers in the graph, modifying the state and updating the UI.
fn render_thumbnail(thumbnail_renders: &mut HashMap<NodeId, Vec<SvgSegment>>, parent_network_node_id: NodeId, graphic: &impl Render, responses: &mut VecDeque<FrontendMessage>) {
// Skip thumbnails if the layer is too complex (for performance)
if graphic.render_complexity() > 1000 {
let old = thumbnail_renders.insert(parent_network_node_id, Vec::new());
@@ -349,12 +347,21 @@ impl NodeRuntime {
return;
}
let bounds = graphic.bounding_box(DAffine2::IDENTITY, true);
let bounds = match graphic.bounding_box(DAffine2::IDENTITY, true) {
RenderBoundingBox::None => return,
RenderBoundingBox::Infinite => [DVec2::ZERO, DVec2::new(300., 200.)],
RenderBoundingBox::Rectangle(bounds) => bounds,
};
let footprint = Footprint {
transform: DAffine2::from_translation(DVec2::new(bounds[0].x, bounds[0].y)),
resolution: UVec2::new((bounds[1].x - bounds[0].x).abs() as u32, (bounds[1].y - bounds[0].y).abs() as u32),
quality: RenderQuality::Full,
};
// Render the thumbnail from a `Graphic` into an SVG string
let render_params = RenderParams {
view_mode: ViewMode::Normal,
culling_bounds: bounds,
footprint,
thumbnail: true,
hide_artboards: false,
for_export: false,
@@ -365,8 +372,7 @@ impl NodeRuntime {
graphic.render_svg(&mut render, &render_params);
// And give the SVG a viewbox and outer <svg>...</svg> wrapper tag
let [min, max] = bounds.unwrap_or_default();
render.format_svg(min, max);
render.format_svg(bounds[0], bounds[1]);
// UPDATE FRONTEND THUMBNAIL