diff --git a/crates/op-design-lint/src/detectors/typography.rs b/crates/op-design-lint/src/detectors/typography.rs index 56d22a1c3..5bdebff2e 100644 --- a/crates/op-design-lint/src/detectors/typography.rs +++ b/crates/op-design-lint/src/detectors/typography.rs @@ -186,6 +186,9 @@ fn first_solid_color(fills: Option<&Vec>) -> Option { } } PenFill::Image(_) => continue, + // Mesh gradients / SkSL shaders have no single representative + // color for contrast checks — treated like image fills. + PenFill::MeshGradient(_) | PenFill::Shader(_) => continue, } } None diff --git a/crates/op-editor-core/src/fills.rs b/crates/op-editor-core/src/fills.rs index 343547b7a..2764791aa 100644 --- a/crates/op-editor-core/src/fills.rs +++ b/crates/op-editor-core/src/fills.rs @@ -348,6 +348,8 @@ pub fn first_solid_fill_opacity(node: &PenNode) -> f32 { PenFill::Solid(b) => b.opacity.unwrap_or(1.0), PenFill::LinearGradient(b) => b.opacity.unwrap_or(1.0), PenFill::RadialGradient(b) => b.opacity.unwrap_or(1.0), + PenFill::MeshGradient(b) => b.opacity.unwrap_or(1.0), + PenFill::Shader(b) => b.opacity.unwrap_or(1.0), PenFill::Image(b) => b.opacity.unwrap_or(1.0), }) .unwrap_or(1.0) @@ -367,6 +369,8 @@ pub fn first_solid_stroke_opacity(node: &PenNode) -> f32 { PenFill::Solid(b) => b.opacity.unwrap_or(1.0), PenFill::LinearGradient(b) => b.opacity.unwrap_or(1.0), PenFill::RadialGradient(b) => b.opacity.unwrap_or(1.0), + PenFill::MeshGradient(b) => b.opacity.unwrap_or(1.0), + PenFill::Shader(b) => b.opacity.unwrap_or(1.0), PenFill::Image(b) => b.opacity.unwrap_or(1.0), }) .unwrap_or(1.0) @@ -475,6 +479,8 @@ pub fn set_primary_fill_opacity(node: &mut PenNode, opacity: f32) -> bool { PenFill::Solid(b) => b.opacity = Some(opacity), PenFill::LinearGradient(b) => b.opacity = Some(opacity), PenFill::RadialGradient(b) => b.opacity = Some(opacity), + PenFill::MeshGradient(b) => b.opacity = Some(opacity), + PenFill::Shader(b) => b.opacity = Some(opacity), PenFill::Image(b) => b.opacity = Some(opacity), } true @@ -618,6 +624,10 @@ pub fn fill_type_of(fill: &PenFill) -> FillType { PenFill::Solid(_) => FillType::Solid, PenFill::LinearGradient(_) => FillType::LinearGradient, PenFill::RadialGradient(_) => FillType::RadialGradient, + // The property panel has no mesh-gradient / shader picker rows + // yet — report the neutral default so the panel stays usable + // without offering a lossy conversion. + PenFill::MeshGradient(_) | PenFill::Shader(_) => FillType::Solid, PenFill::Image(_) => FillType::Image, } } @@ -692,6 +702,9 @@ fn fill_hex(fill: &PenFill) -> Option<&str> { PenFill::Solid(body) => Some(body.color.as_str()), PenFill::LinearGradient(body) => body.stops.first().map(|s| s.color.as_str()), PenFill::RadialGradient(body) => body.stops.first().map(|s| s.color.as_str()), + PenFill::MeshGradient(body) => body.stops.first().map(|s| s.color.as_str()), + // A shader has no representative colour. + PenFill::Shader(_) => None, PenFill::Image(_) => None, } } diff --git a/crates/op-editor-ui/src/widgets/canvas_viewport_overlay.rs b/crates/op-editor-ui/src/widgets/canvas_viewport_overlay.rs index 987d29384..24aef582d 100644 --- a/crates/op-editor-ui/src/widgets/canvas_viewport_overlay.rs +++ b/crates/op-editor-ui/src/widgets/canvas_viewport_overlay.rs @@ -242,6 +242,18 @@ pub(crate) fn paint_gradient_rect( *opacity, ); } + SceneGradient::Mesh { + colors, opacity, .. + } => { + // No mesh-gradient method on the RenderBackend yet — + // degrade to the first vertex colour (same fallback the + // scene builder bakes into `SceneNode.fill`). + if let Some(first) = colors.first() { + let mut c = *first; + c.a = (c.a * opacity).clamp(0.0, 1.0); + cx.backend.fill_round_rect(rect, corner_radius, c); + } + } } } diff --git a/crates/op-editor-ui/src/widgets/canvas_viewport_paint.rs b/crates/op-editor-ui/src/widgets/canvas_viewport_paint.rs index bd2a65a61..06ec8cab0 100644 --- a/crates/op-editor-ui/src/widgets/canvas_viewport_paint.rs +++ b/crates/op-editor-ui/src/widgets/canvas_viewport_paint.rs @@ -729,7 +729,9 @@ pub(crate) fn paint_svg_path_node( d, world_rect, &flat, *gx, *cy, *radius, *opacity, ); } - None => { + // Mesh gradients have no svg-path shader yet — fall back to + // the node's resolved `fill` colour like the no-gradient path. + Some(SceneGradient::Mesh { .. }) | None => { if let Some(fill) = node.fill { cx.backend.fill_svg_path_in_rect(d, world_rect, fill); } diff --git a/crates/op-editor-ui/src/widgets/property_panel_snapshot.rs b/crates/op-editor-ui/src/widgets/property_panel_snapshot.rs index 401426de2..ae5ef855a 100644 --- a/crates/op-editor-ui/src/widgets/property_panel_snapshot.rs +++ b/crates/op-editor-ui/src/widgets/property_panel_snapshot.rs @@ -1160,6 +1160,12 @@ fn fills_of(node: &PenNode) -> Vec { b.stops.first().map(|s| s.color.as_str()), b.opacity.unwrap_or(1.0), ), + PenFill::MeshGradient(b) => ( + b.stops.first().map(|s| s.color.as_str()), + b.opacity.unwrap_or(1.0), + ), + // Shader fills have no representative colour. + PenFill::Shader(b) => (None, b.opacity.unwrap_or(1.0)), PenFill::Image(b) => (None, b.opacity.unwrap_or(1.0)), }; FillSummary { diff --git a/crates/op-orchestrator/src/variable_binding.rs b/crates/op-orchestrator/src/variable_binding.rs index e42cd3465..b24e3ba6f 100644 --- a/crates/op-orchestrator/src/variable_binding.rs +++ b/crates/op-orchestrator/src/variable_binding.rs @@ -105,6 +105,14 @@ fn bind_fills(fills: &mut [PenFill], refs: &ColorRefs) { bind_color_string(&mut stop.color, refs); } } + PenFill::MeshGradient(body) => { + for stop in &mut body.stops { + bind_color_string(&mut stop.color, refs); + } + } + // Shader colours live in untyped uniform values — leave + // them alone rather than guess which uniforms are colours. + PenFill::Shader(_) => {} PenFill::Image(_) => {} } } diff --git a/crates/op-pen-loader/src/layout_scene.rs b/crates/op-pen-loader/src/layout_scene.rs index c1449a54b..7c36ee50b 100644 --- a/crates/op-pen-loader/src/layout_scene.rs +++ b/crates/op-pen-loader/src/layout_scene.rs @@ -216,6 +216,9 @@ fn node_payload_to_scene( .gradient .as_ref() .map(|g| scale_gradient_opacity(payload_gradient_to_scene(g), cum_opacity)), + // The payload path doesn't carry SkSL shader bodies yet — the + // scene painter degrades to the resolved `fill` colour. + shader: None, stroke: node.stroke.as_ref().map(|s| { let mut st = scene_stroke(s, &node_id, var_table); st.color = mul_alpha(st.color, cum_opacity); @@ -377,6 +380,17 @@ fn scale_gradient_opacity(g: SceneGradient, k: f32) -> SceneGradient { opacity: (opacity * k).clamp(0.0, 1.0), stops, }, + SceneGradient::Mesh { + rows, + cols, + colors, + opacity, + } => SceneGradient::Mesh { + rows, + cols, + colors, + opacity: (opacity * k).clamp(0.0, 1.0), + }, } } diff --git a/crates/op-pen-loader/src/style_payload.rs b/crates/op-pen-loader/src/style_payload.rs index 731195cd3..927deacb0 100644 --- a/crates/op-pen-loader/src/style_payload.rs +++ b/crates/op-pen-loader/src/style_payload.rs @@ -255,6 +255,18 @@ fn first_solid_color(fills: Option<&[PenFill]>) -> Option<[f32; 4]> { } } } + PenFill::MeshGradient(body) => { + if let Some(stop) = body.stops.first() { + if let Some(rgba) = parse_hex(&stop.color) { + return Some(apply_alpha(rgba, body.opacity)); + } + } + } + PenFill::Shader(body) => { + // Payload painters can't run SkSL — mid-gray fallback + // (mirrors SceneShader::fallback's default). + return Some(apply_alpha([0.5, 0.5, 0.5, 1.0], body.opacity)); + } PenFill::Image(_) => { return Some([0.85, 0.86, 0.88, 1.0]); } diff --git a/vendor/jian b/vendor/jian index 8ed9a89fa..ff39a4163 160000 --- a/vendor/jian +++ b/vendor/jian @@ -1 +1 @@ -Subproject commit 8ed9a89fa46b13bb26524e3176b708b2ed6b2970 +Subproject commit ff39a4163edee286fa7bd8c6afc969bcca2cbaec