From 48fb945518a46e306a6d90e0604c2b5b8285c1fe Mon Sep 17 00:00:00 2001 From: YohYamasaki Date: Sun, 19 Apr 2026 22:37:52 +0900 Subject: [PATCH 1/2] wip: add mesh enum --- .../document/node_graph/node_properties.rs | 2 +- .../tool/tool_messages/gradient_tool.rs | 8 +++- .../libraries/rendering/src/render_ext.rs | 1 + .../libraries/rendering/src/renderer.rs | 26 +++++++++- .../libraries/vector-types/src/gradient.rs | 1 + node-graph/nodes/vector/src/vector_nodes.rs | 47 +++++++++++++++++++ 6 files changed, 82 insertions(+), 3 deletions(-) diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index 3a2c6a778b..9bcab4679e 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -1943,7 +1943,7 @@ pub(crate) fn fill_properties(node_id: NodeId, context: &mut NodePropertiesConte if let Fill::Gradient(gradient) = fill.clone() { let mut row = vec![TextLabel::new("").widget_instance()]; match gradient.gradient_type { - GradientType::Linear => add_blank_assist(&mut row), + GradientType::Linear | GradientType::Mesh => add_blank_assist(&mut row), GradientType::Radial => { let orientation = if (gradient.end.x - gradient.start.x).abs() > f64::EPSILON * 1e6 { gradient.end.x > gradient.start.x diff --git a/editor/src/messages/tool/tool_messages/gradient_tool.rs b/editor/src/messages/tool/tool_messages/gradient_tool.rs index f6b8f49be9..d77c02ed74 100644 --- a/editor/src/messages/tool/tool_messages/gradient_tool.rs +++ b/editor/src/messages/tool/tool_messages/gradient_tool.rs @@ -174,8 +174,14 @@ impl LayoutHolder for GradientTool { } .into() }), + RadioEntryData::new("Mesh").label("Mesh").tooltip_label(" Gradient").on_update(move |_| { + GradientToolMessage::UpdateOptions { + options: GradientOptionsUpdate::Type(GradientType::Mesh), + } + .into() + }), ]) - .selected_index(Some((self.options.gradient_type == GradientType::Radial) as u32)) + .selected_index(Some(self.options.gradient_type as u32)) .widget_instance(); let reverse_stops = IconButton::new("Reverse", 24) diff --git a/node-graph/libraries/rendering/src/render_ext.rs b/node-graph/libraries/rendering/src/render_ext.rs index df1690afd2..d48a608752 100644 --- a/node-graph/libraries/rendering/src/render_ext.rs +++ b/node-graph/libraries/rendering/src/render_ext.rs @@ -72,6 +72,7 @@ impl RenderExt for Gradient { gradient_id, start.x, start.y, radius, stop ); } + _ => (), } gradient_id diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index 6f1690ad37..ee952e3cb0 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -24,7 +24,7 @@ use std::fmt::Write; use std::hash::{Hash, Hasher}; use std::ops::Deref; use std::sync::{Arc, LazyLock}; -use vector_types::gradient::GradientSpreadMethod; +use vector_types::gradient::{self, GradientSpreadMethod}; use vello::*; /// Cached 16x16 transparency checkerboard image data (two 8x8 cells of #ffffff and #cccccc). @@ -967,6 +967,15 @@ impl Render for Table { } } + let is_mesh_fill = matches!( + &row.element.style.fill, + Fill::Gradient(g) if g.gradient_type == GradientType::Mesh + ); + if is_mesh_fill { + log::debug!("hoge"); + return; + } + render.leaf_tag("path", |attributes| { attributes.push("d", path.clone()); let matrix = format_transform_matrix(element_transform); @@ -1159,6 +1168,12 @@ impl Render for Table { } .into() } + // TODO!!!!!!!!!!!!!!!! + GradientType::Mesh => peniko::LinearGradientPosition { + start: to_point(start), + end: to_point(end), + } + .into(), }, extend: match gradient.spread_method { GradientSpreadMethod::Pad => peniko::Extend::Pad, @@ -1752,6 +1767,15 @@ impl Render for Table { r#"{stop_string}"# ); } + // TODO!!!!!!!!!!!!!!!!!!!!!! + GradientType::Mesh => { + let (x1, y1) = (start.x, start.y); + let (x2, y2) = (end.x, end.y); + let _ = write!( + &mut attributes.0.svg_defs, + r#"{stop_string}"# + ); + } } attributes.push("fill", format!("url('#{gradient_id}')")); diff --git a/node-graph/libraries/vector-types/src/gradient.rs b/node-graph/libraries/vector-types/src/gradient.rs index f5c241c2f0..a9642c628d 100644 --- a/node-graph/libraries/vector-types/src/gradient.rs +++ b/node-graph/libraries/vector-types/src/gradient.rs @@ -9,6 +9,7 @@ pub enum GradientType { #[default] Linear, Radial, + Mesh, } // TODO: Someday we could switch this to a Box[T] to avoid over-allocation diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index 40fb786b06..baa2c8175d 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -14,6 +14,7 @@ use kurbo::simplify::{SimplifyOptions, simplify_bezpath}; use kurbo::{Affine, BezPath, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveArclen, PathEl, PathSeg, Shape}; use rand::{Rng, SeedableRng}; use std::collections::hash_map::DefaultHasher; +use std::vec; use vector_types::subpath::{BezierHandles, ManipulatorGroup}; use vector_types::vector::PointDomain; use vector_types::vector::algorithms::bezpath_algorithms::{self, TValue, eval_pathseg_euclidean, evaluate_bezpath, split_bezpath, tangent_on_bezpath}; @@ -26,6 +27,7 @@ use vector_types::vector::misc::{ }; use vector_types::vector::style::{Fill, Gradient, GradientStops, PaintOrder, Stroke, StrokeAlign, StrokeCap, StrokeJoin}; use vector_types::vector::{FillId, PointId, RegionId, SegmentDomain, SegmentId, StrokeId, VectorExt}; +use wasm_bindgen::UnwrapThrowExt; /// Implemented for types that can be converted to an iterator of vector rows. /// Used for the fill and stroke node so they can be used on `Table` or `Table`. @@ -107,6 +109,51 @@ where content } +// #[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("fill_properties"))] +// async fn mesh_gradient( +// _: impl Ctx, +// /// The content with vector paths to apply the mesh gradient to. +// mut content: Table, +// /// The top-left color of the gradient. +// #[default(Color::BLACK)] +// top_left_color: Table, +// /// The top-right color of the gradient. +// #[default(Color::BLACK)] +// top_right_color: Table, +// /// The bottom-left color of the gradient. +// #[default(Color::BLACK)] +// bottom_left_color: Table, +// /// The bottom-right color of the gradient. +// #[default(Color::BLACK)] +// bottom_right_color: Table, +// _backup_color: Table, +// _backup_gradient: Gradient, +// ) -> Table { +// for vector in content.vector_iter_mut() { +// let grid_num = 8; +// let grid_stride_uv = 1. / grid_num as f64; + +// let segments: Vec<_> = vector.element.segment_iter().collect(); +// let points = vector.element.point_domain.positions(); +// let top_seg = segments.get(0).unwrap_throw().1; +// let right_seg = segments.get(1).unwrap_throw().1; +// let bottom_seg = segments.get(2).unwrap_throw().1; +// let left_seg = segments.get(3).unwrap_throw().1; + +// let bilerp = |u: f64, v: f64| points[0] * (1. - u) * (1. - v) + points[1] * u * (1. - v) + points[2] * (1. - u) * v + points[3] * u * v; + +// for i in 0..=grid_num { +// for j in 0..=grid_num { +// let u = i as f64 * grid_stride_uv; +// let v = j as f64 * grid_stride_uv; +// let pos = point_to_dvec2(top_seg.eval(u)) + point_to_dvec2(bottom_seg.eval(1. - u)) - bilerp(u, v); +// } +// } +// } + +// content +// } + /// Applies a fill style to the vector content, giving an appearance to the area within the interior of the geometry. #[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("fill_properties"))] async fn fill + 'n + Send, V: VectorTableIterMut + 'n + Send>( From 9148ad8b0f89c920fd86f486858266d16369753a Mon Sep 17 00:00:00 2001 From: Yohei <74522538+YohyYamasaki@users.noreply.github.com> Date: Mon, 20 Apr 2026 13:11:57 +0900 Subject: [PATCH 2/2] wip: first poc Approximate Coons patch by creating bilinear gradient in parallelograms. Each subgrid's color does not have C1 continuity, so we can see steps of colors in the strongly skewed scene. This could be solved by using bicubic interpolation. --- .../no-std-types/src/color/color_types.rs | 14 ++ .../libraries/rendering/src/renderer.rs | 123 +++++++++++++++++- node-graph/nodes/vector/src/vector_nodes.rs | 45 ------- 3 files changed, 134 insertions(+), 48 deletions(-) diff --git a/node-graph/libraries/no-std-types/src/color/color_types.rs b/node-graph/libraries/no-std-types/src/color/color_types.rs index abcb1e9e86..ab3fe1b34a 100644 --- a/node-graph/libraries/no-std-types/src/color/color_types.rs +++ b/node-graph/libraries/no-std-types/src/color/color_types.rs @@ -1101,6 +1101,20 @@ impl Color { } } +pub fn bilerp_color(c00: Color, c10: Color, c01: Color, c11: Color, u: f64, v: f64) -> Color { + let (u, v) = (u as f32, v as f32); + let w00 = (1. - u) * (1. - v); + let w10 = u * (1. - v); + let w01 = (1. - u) * v; + let w11 = u * v; + Color::from_rgbaf32_unchecked( + w00 * c00.r() + w10 * c10.r() + w01 * c01.r() + w11 * c11.r(), + w00 * c00.g() + w10 * c10.g() + w01 * c01.g() + w11 * c11.g(), + w00 * c00.b() + w10 * c10.b() + w01 * c01.b() + w11 * c11.b(), + w00 * c00.a() + w10 * c10.a() + w01 * c01.a() + w11 * c11.a(), + ) +} + #[cfg(test)] mod tests { use super::*; diff --git a/node-graph/libraries/rendering/src/renderer.rs b/node-graph/libraries/rendering/src/renderer.rs index ee952e3cb0..37961b7205 100644 --- a/node-graph/libraries/rendering/src/renderer.rs +++ b/node-graph/libraries/rendering/src/renderer.rs @@ -2,7 +2,7 @@ use crate::render_ext::RenderExt; use crate::to_peniko::BlendModeExt; use core_types::blending::BlendMode; use core_types::bounds::{BoundingBox, RenderBoundingBox}; -use core_types::color::{Alpha, Color}; +use core_types::color::{Alpha, Color, bilerp_color}; use core_types::math::quad::Quad; use core_types::render_complexity::RenderComplexity; use core_types::table::{Table, TableRow}; @@ -17,7 +17,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, Cap, Join, Shape}; +use kurbo::{Affine, Cap, Join, ParamCurve, Shape}; use num_traits::Zero; use std::collections::{HashMap, HashSet}; use std::fmt::Write; @@ -25,6 +25,7 @@ use std::hash::{Hash, Hasher}; use std::ops::Deref; use std::sync::{Arc, LazyLock}; use vector_types::gradient::{self, GradientSpreadMethod}; +use vector_types::vector::misc::point_to_dvec2; use vello::*; /// Cached 16x16 transparency checkerboard image data (two 8x8 cells of #ffffff and #cccccc). @@ -972,7 +973,123 @@ impl Render for Table { Fill::Gradient(g) if g.gradient_type == GradientType::Mesh ); if is_mesh_fill { - log::debug!("hoge"); + // Split the mesh into 8 x 8 subgrids + let grid_num = 8; + let grid_stride_uv = 1. / grid_num as f64; + + let segments: Vec<_> = row.element.segment_iter().map(|(_, path_seg, _, _)| path_seg).collect(); + let points = row.element.point_domain.positions(); + + let top_seg = segments[0]; + let right_seg = segments[1]; + let bottom_seg = segments[2]; + let left_seg = segments[3]; + + let top_left_color = Color::RED; + let top_right_color = Color::BLUE; + let bottom_left_color = Color::GREEN; + let bottom_right_color = Color::YELLOW; + + let bilerp = |u: f64, v: f64| points[3] * (1. - u) * (1. - v) + points[2] * u * (1. - v) + points[0] * (1. - u) * v + points[1] * u * v; + + // Create the position and color info of the grid that is splitted from the original mesh + let mut grid_info: Vec> = vec![]; + + for i in 0..=grid_num { + let u = i as f64 * grid_stride_uv; + let mut grid_info_row: Vec<(DVec2, Color)> = vec![]; + + for j in 0..=grid_num { + let v = j as f64 * grid_stride_uv; + let c1_u = point_to_dvec2(bottom_seg.eval(1. - u)); + let c2_u = point_to_dvec2(top_seg.eval(u)); + let d1_v = point_to_dvec2(left_seg.eval(v)); + let d2_v = point_to_dvec2(right_seg.eval(1. - v)); + let lc = (1. - v) * c1_u + v * c2_u; + let ld = (1. - u) * d1_v + u * d2_v; + + let pos = lc + ld - bilerp(u, v); + let color = bilerp_color(bottom_left_color, bottom_right_color, top_left_color, top_right_color, u, v); + grid_info_row.push((pos, color)); + } + grid_info.push(grid_info_row); + } + + // Create poloygons using the vertex position data + let mut idx = generate_uuid(); + + for i in 0..grid_num { + for j in 0..grid_num { + let bl = grid_info[i][j]; + let tl = grid_info[i][j + 1]; + let br = grid_info[i + 1][j]; + let tr = grid_info[i + 1][j + 1]; + + // linear gradient for the bottom line + write!( + &mut render.svg_defs, + r##""##, + bl.0.x, + bl.0.y, + br.0.x, + br.0.y, + bl.1.to_rgb_hex_srgb_from_gamma(), + br.1.to_rgb_hex_srgb_from_gamma(), + ) + .unwrap(); + + // linear gradient for the top line + write!( + &mut render.svg_defs, + r##""##, + tl.0.x, + tl.0.y, + tr.0.x, + tr.0.y, + tl.1.to_rgb_hex_srgb_from_gamma(), + tr.1.to_rgb_hex_srgb_from_gamma(), + ) + .unwrap(); + + // top mask gradient + write!( + &mut render.svg_defs, + r##""##, + (bl.0.x + br.0.x) / 2., + (bl.0.y + br.0.y) / 2., + (tl.0.x + tr.0.x) / 2., + (tl.0.y + tr.0.y) / 2., + ) + .unwrap(); + + // mask + write!( + &mut render.svg_defs, + r##""##, + bl.0.x, bl.0.y, br.0.x, br.0.y, tr.0.x, tr.0.y, tl.0.x, tl.0.y, + ) + .unwrap(); + + render.leaf_tag("polygon", |attributes| { + attributes.push("points", format!("{},{} {},{} {},{} {},{}", bl.0.x, bl.0.y, br.0.x, br.0.y, tr.0.x, tr.0.y, tl.0.x, tl.0.y)); + attributes.push("fill", format!("url(#gb{idx})")); + }); + + render.leaf_tag("polygon", |attributes| { + attributes.push("points", format!("{},{} {},{} {},{} {},{}", bl.0.x, bl.0.y, br.0.x, br.0.y, tr.0.x, tr.0.y, tl.0.x, tl.0.y)); + attributes.push("fill", format!("url(#gt{idx})")); + attributes.push("mask", format!("url(#m{idx})")); + }); + + idx += 1; + } + } + + // Create a linear gradients, bottom-left -> bottom-right + + // Create a linear gradient mask from top to bottom + + // Create a linear gradient, top->left -> top->right with applying the mask return; } diff --git a/node-graph/nodes/vector/src/vector_nodes.rs b/node-graph/nodes/vector/src/vector_nodes.rs index baa2c8175d..fbbe288a07 100644 --- a/node-graph/nodes/vector/src/vector_nodes.rs +++ b/node-graph/nodes/vector/src/vector_nodes.rs @@ -109,51 +109,6 @@ where content } -// #[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("fill_properties"))] -// async fn mesh_gradient( -// _: impl Ctx, -// /// The content with vector paths to apply the mesh gradient to. -// mut content: Table, -// /// The top-left color of the gradient. -// #[default(Color::BLACK)] -// top_left_color: Table, -// /// The top-right color of the gradient. -// #[default(Color::BLACK)] -// top_right_color: Table, -// /// The bottom-left color of the gradient. -// #[default(Color::BLACK)] -// bottom_left_color: Table, -// /// The bottom-right color of the gradient. -// #[default(Color::BLACK)] -// bottom_right_color: Table, -// _backup_color: Table, -// _backup_gradient: Gradient, -// ) -> Table { -// for vector in content.vector_iter_mut() { -// let grid_num = 8; -// let grid_stride_uv = 1. / grid_num as f64; - -// let segments: Vec<_> = vector.element.segment_iter().collect(); -// let points = vector.element.point_domain.positions(); -// let top_seg = segments.get(0).unwrap_throw().1; -// let right_seg = segments.get(1).unwrap_throw().1; -// let bottom_seg = segments.get(2).unwrap_throw().1; -// let left_seg = segments.get(3).unwrap_throw().1; - -// let bilerp = |u: f64, v: f64| points[0] * (1. - u) * (1. - v) + points[1] * u * (1. - v) + points[2] * (1. - u) * v + points[3] * u * v; - -// for i in 0..=grid_num { -// for j in 0..=grid_num { -// let u = i as f64 * grid_stride_uv; -// let v = j as f64 * grid_stride_uv; -// let pos = point_to_dvec2(top_seg.eval(u)) + point_to_dvec2(bottom_seg.eval(1. - u)) - bilerp(u, v); -// } -// } -// } - -// content -// } - /// Applies a fill style to the vector content, giving an appearance to the area within the interior of the geometry. #[node_macro::node(category("Vector: Style"), path(graphene_core::vector), properties("fill_properties"))] async fn fill + 'n + Send, V: VectorTableIterMut + 'n + Send>(