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 <dennis@kobert.dev>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
This commit is contained in:
YohYamasaki 2026-03-10 12:23:48 +01:00 committed by GitHub
parent 3a7a5f5953
commit 8e52309bb0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 69 additions and 29 deletions

View File

@ -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::<PointId>::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<Vector> {
}
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<Vector> {
// 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<Raster<CPU>> {
}
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<Raster<CPU>> {
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<Raster<CPU>> {
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<Raster<CPU>> {
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<Raster<GPU>> {
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 {