From 8e52309bb08b5234a919e3767f9aaffabb22a05e Mon Sep 17 00:00:00 2001 From: YohYamasaki <74522538+YohYamasaki@users.noreply.github.com> Date: Tue, 10 Mar 2026 12:23:48 +0100 Subject: [PATCH] Render raster images as outlines in Outline mode (#3831) * Render raster images as outlines in Outline mode * Draw a transformed unit-rectangle stroke instead of raster pixels * Skip creating blend layers for a raster image in Outline mode when only blend mode would trigger them * Rename variable names * Minor refactor to reduce nesting * Extract shared outline drawing helper * Update node-graph/libraries/rendering/src/renderer.rs Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --------- Co-authored-by: Dennis Kobert Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --- .../libraries/rendering/src/renderer.rs | 98 +++++++++++++------ 1 file changed, 69 insertions(+), 29 deletions(-) diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index fc51b500..5b585827 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -18,8 +18,7 @@ use graphic_types::vector_types::subpath::Subpath; use graphic_types::vector_types::vector::click_target::{ClickTarget, FreePoint}; use graphic_types::vector_types::vector::style::{Fill, PaintOrder, RenderMode, Stroke, StrokeAlign}; use graphic_types::{Artboard, Graphic}; -use kurbo::Affine; -use kurbo::Shape; +use kurbo::{Affine, Cap, Join, Shape}; use num_traits::Zero; use std::collections::{HashMap, HashSet}; use std::fmt::Write; @@ -262,6 +261,36 @@ pub fn to_transform(transform: DAffine2) -> usvg::Transform { usvg::Transform::from_row(cols[0] as f32, cols[1] as f32, cols[2] as f32, cols[3] as f32, cols[4] as f32, cols[5] as f32) } +fn get_outline_styles(render_params: &RenderParams) -> (kurbo::Stroke, peniko::Color) { + use core_types::consts::LAYER_OUTLINE_STROKE_WEIGHT; + + let outline_stroke = kurbo::Stroke { + width: LAYER_OUTLINE_STROKE_WEIGHT / if render_params.viewport_zoom > 0. { render_params.viewport_zoom } else { 1. }, + miter_limit: 4., + join: Join::Miter, + start_cap: Cap::Butt, + end_cap: Cap::Butt, + dash_pattern: Default::default(), + dash_offset: 0., + }; + + let outline_color = black_or_white_for_best_contrast(render_params.artboard_background); + let outline_color_peniko = peniko::Color::new([outline_color.r(), outline_color.g(), outline_color.b(), outline_color.a()]); + + (outline_stroke, outline_color_peniko) +} + +fn draw_raster_outline(scene: &mut Scene, outline_transform: &DAffine2, render_params: &RenderParams) { + use graphic_types::vector_types::vector::PointId; + + let (outline_stroke, outline_color_peniko) = get_outline_styles(render_params); + + let mut outline_path = Subpath::::new_rectangle(DVec2::ZERO, DVec2::ONE).to_bezpath(); + outline_path.apply_affine(Affine::new(outline_transform.to_cols_array())); + + scene.stroke(&outline_stroke, Affine::IDENTITY, outline_color_peniko, None, &outline_path); +} + // TODO: Click targets can be removed from the render output, since the vector data is available in the vector modify data from Monitor nodes. // This will require that the transform for child layers into that layer space be calculated, or it could be returned from the RenderOutput instead of click targets. #[derive(Debug, Default, Clone, PartialEq, DynAny, serde::Serialize, serde::Deserialize)] @@ -935,10 +964,7 @@ impl Render for Table { } fn render_to_vello(&self, scene: &mut Scene, parent_transform: DAffine2, _context: &mut RenderContext, render_params: &RenderParams) { - use core_types::consts::LAYER_OUTLINE_STROKE_WEIGHT; use graphic_types::vector_types::vector::style::{GradientType, StrokeCap, StrokeJoin}; - use vello::kurbo::{Cap, Join}; - use vello::peniko; for row in self.iter() { use graphic_types::vector_types::vector; @@ -1111,20 +1137,9 @@ impl Render for Table { // Render the path match render_params.render_mode { RenderMode::Outline => { - let outline_stroke = kurbo::Stroke { - width: LAYER_OUTLINE_STROKE_WEIGHT / if render_params.viewport_zoom > 0. { render_params.viewport_zoom } else { 1. }, - miter_limit: 4., - join: Join::Miter, - start_cap: Cap::Butt, - end_cap: Cap::Butt, - dash_pattern: Default::default(), - dash_offset: 0., - }; + let (outline_stroke, outline_color_peniko) = get_outline_styles(render_params); - let outline_color = black_or_white_for_best_contrast(render_params.artboard_background); - let outline_color = peniko::Color::new([outline_color.r(), outline_color.g(), outline_color.b(), outline_color.a()]); - - scene.stroke(&outline_stroke, kurbo::Affine::new(element_transform.to_cols_array()), outline_color, None, &path); + scene.stroke(&outline_stroke, kurbo::Affine::new(element_transform.to_cols_array()), outline_color_peniko, None, &path); } _ => { if use_layer { @@ -1375,8 +1390,6 @@ impl Render for Table> { } fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, _: &mut RenderContext, render_params: &RenderParams) { - use vello::peniko; - for row in self.iter() { let image = &row.element; if image.data.is_empty() { @@ -1389,7 +1402,7 @@ impl Render for Table> { let opacity = alpha_blending.opacity(render_params.for_mask); let mut layer = false; - if (opacity < 1. || alpha_blending.blend_mode != BlendMode::default()) + if (opacity < 1. || (render_params.render_mode != RenderMode::Outline && alpha_blending.blend_mode != BlendMode::default())) && let RenderBoundingBox::Rectangle(bounds) = self.bounding_box(transform, false) { let blending = peniko::BlendMode::new(blend_mode, peniko::Compose::SrcOver); @@ -1398,6 +1411,19 @@ impl Render for Table> { layer = true; } + if let RenderMode::Outline = render_params.render_mode { + let outline_transform = transform * *row.transform; + draw_raster_outline(scene, &outline_transform, render_params); + + if layer { + scene.pop_layer(); + } + + continue; + } + + let image_transform = transform * *row.transform * DAffine2::from_scale(1. / DVec2::new(image.width as f64, image.height as f64)); + let image_brush = peniko::ImageBrush::new(peniko::ImageData { data: image.to_flat_u8().0.into(), format: peniko::ImageFormat::Rgba8, @@ -1406,7 +1432,6 @@ impl Render for Table> { alpha_type: peniko::ImageAlphaType::Alpha, }) .with_extend(peniko::Extend::Repeat); - let image_transform = transform * *row.transform * DAffine2::from_scale(1. / DVec2::new(image.width as f64, image.height as f64)); scene.draw_image(&image_brush, kurbo::Affine::new(image_transform.to_cols_array())); @@ -1441,21 +1466,36 @@ impl Render for Table> { log::warn!("tried to render texture as an svg"); } - fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, _render_params: &RenderParams) { - use vello::peniko; - + fn render_to_vello(&self, scene: &mut Scene, transform: DAffine2, context: &mut RenderContext, render_params: &RenderParams) { for row in self.iter() { - let blend_mode = *row.alpha_blending; + let alpha_blending = *row.alpha_blending; + let blend_mode = match render_params.render_mode { + RenderMode::Outline => peniko::Mix::Normal, + _ => alpha_blending.blend_mode.to_peniko(), + }; + let mut layer = false; - if blend_mode != Default::default() + + if (render_params.render_mode != RenderMode::Outline && alpha_blending != Default::default()) && let RenderBoundingBox::Rectangle(bounds) = self.bounding_box(transform, true) { - let blending = peniko::BlendMode::new(blend_mode.blend_mode.to_peniko(), peniko::Compose::SrcOver); + let blending = peniko::BlendMode::new(blend_mode, peniko::Compose::SrcOver); let rect = kurbo::Rect::new(bounds[0].x, bounds[0].y, bounds[1].x, bounds[1].y); - scene.push_layer(peniko::Fill::NonZero, blending, blend_mode.opacity, kurbo::Affine::IDENTITY, &rect); + scene.push_layer(peniko::Fill::NonZero, blending, alpha_blending.opacity, kurbo::Affine::IDENTITY, &rect); layer = true; } + if let RenderMode::Outline = render_params.render_mode { + let outline_transform = transform * *row.transform; + draw_raster_outline(scene, &outline_transform, render_params); + + if layer { + scene.pop_layer(); + } + + continue; + } + let width = row.element.data().width(); let height = row.element.data().height(); let image = peniko::ImageBrush::new(peniko::ImageData {