From f681284dfb01d0ec99e2322993a8c465036b09f1 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 14 Nov 2026 16:40:34 -0800 Subject: [PATCH 1/3] 3D: art at NaN or infinite opacity casts no shadow Extrude & Bevel, Inflate and Rotate decided whether a fill, stroke or member casts part of the shadow with `opacity > 0.0` and skipped it with `opacity <= 0.0`. A NaN opacity failed both tests, so the part was neither cast nor skipped: the shadow cast from the solids drawn added its faces with a NaN ink, giving the shadow fill a NaN opacity, and the shadow built for itself recorded an empty part. Both paths now use one rule, `shadow::Ink::casts`: art casts only when its opacity is a finite number above zero. NaN and infinite opacity cast nothing, as 0 % does, and the art beside it still casts. Finite opacity casts as before. --- crates/effects/src/solids.rs | 20 ++++---- crates/effects/src/solids/shadow.rs | 8 +++ crates/effects/src/solids/tests_shadow.rs | 60 +++++++++++++++++++++++ docs/target-app-parity.md | 1 + 4 files changed, 79 insertions(+), 10 deletions(-) diff --git a/crates/effects/src/solids.rs b/crates/effects/src/solids.rs index c0d44947d7..6fad9eac9c 100644 --- a/crates/effects/src/solids.rs +++ b/crates/effects/src/solids.rs @@ -1457,8 +1457,8 @@ fn cast_shadow<'a>(id: &str, p: &Value, shapes: &'a [Profile]) -> Result = vec![]; let ink = |item: &AppearanceItem, member: f32| { let (_, opacity, blend) = paint_of(item, member); @@ -1466,7 +1466,7 @@ fn cast_shadow<'a>(id: &str, p: &Value, shapes: &'a [Profile]) -> Result 0.0) { + for item in s.items.iter().filter(|i| ink(i, s.opacity).casts()) { let (area, rule) = region(&s.path, s.rule, item); #[cfg(test)] SOLIDS_BUILT.with(|n| n.set(n.get() + 1)); @@ -1480,7 +1480,7 @@ fn cast_shadow<'a>(id: &str, p: &Value, shapes: &'a [Profile]) -> Result, String>> = if id == INFLATE { let shapes: Vec<(&PathData, FillRule)> = parts.iter().map(|(_, _, area, rule, _, _)| (area, *rule)).collect(); - let casts: Vec = inks.iter().map(|i| i.opacity > 0.0).collect(); + let casts: Vec = inks.iter().map(|i| i.casts()).collect(); #[cfg(test)] SOLIDS_BUILT.with(|n| n.set(n.get() + shapes.len())); caster.inflate(&shapes, &casts, bounds, inflate_options(p), view) @@ -1488,13 +1488,13 @@ fn cast_shadow<'a>(id: &str, p: &Value, shapes: &'a [Profile]) -> Result 0.0))); - added.push(if ink.opacity > 0.0 { caster.extrude(area, *rule, bounds, extrude_options(p), view) } else { Ok(0..0) }); + SOLIDS_BUILT.with(|n| n.set(n.get() + usize::from(ink.casts()))); + added.push(if ink.casts() { caster.extrude(area, *rule, bounds, extrude_options(p), view) } else { Ok(0..0) }); } added }; for (((item, _, _, _, scopes, _), ink), faces) in parts.iter().zip(inks).zip(added) { - if ink.opacity <= 0.0 { + if !ink.casts() { continue; } // A stroke whose own solid is past the limits is left out; past the shadow's limit for @@ -1535,7 +1535,7 @@ fn cast_built<'a>(id: &str, p: &Value, shapes: &'a [Profile], built: Vec(id: &str, p: &Value, shapes: &'a [Profile], built: Vec = parts.iter().map(|(item, member, ..)| ink(item, *member)).collect(); - if parts.len() != built.len() || (id == INFLATE && inks.iter().zip(&built).any(|(ink, kept)| ink.opacity > 0.0 && kept.is_none())) { + if parts.len() != built.len() || (id == INFLATE && inks.iter().zip(&built).any(|(ink, kept)| ink.casts() && kept.is_none())) { return cast_shadow(id, p, shapes); } for (((item, _, area, rule, scopes, _), ink), kept) in parts.iter().zip(inks).zip(built) { - if ink.opacity <= 0.0 { + if !ink.casts() { continue; } let faces = match kept { diff --git a/crates/effects/src/solids/shadow.rs b/crates/effects/src/solids/shadow.rs index 3906ac2d3c..15a8e9d3c1 100644 --- a/crates/effects/src/solids/shadow.rs +++ b/crates/effects/src/solids/shadow.rs @@ -50,6 +50,14 @@ pub(crate) struct Ink { pub(crate) blend: BlendMode, } +impl Ink { + /// Whether art in this ink casts part of a shadow: its opacity is a finite number above zero. + /// Art at zero, negative, NaN or infinite opacity casts nothing. + pub(crate) fn casts(self) -> bool { + self.opacity.is_finite() && self.opacity > 0.0 + } +} + /// The shadow of the faces in `caster` as a path of object `id`, cast by art in `ink`: filled /// black at [`SHADOW_OPACITY`] times its opacity, in its blending mode, with the nonzero rule; /// `None` when it casts nothing. diff --git a/crates/effects/src/solids/tests_shadow.rs b/crates/effects/src/solids/tests_shadow.rs index 4d62961612..7dfd78f28d 100644 --- a/crates/effects/src/solids/tests_shadow.rs +++ b/crates/effects/src/solids/tests_shadow.rs @@ -312,3 +312,63 @@ fn the_shadow_cast_from_the_solids_drawn_is_the_shadow_of_solids_built_for_it() } } } + +/// The opacity of each part of the shadow of `n` (its 3D effect's shadow on) and the shadow's art, +/// as [`cast_shadow`] casts it from solids built for the shadow alone and as [`cast_built`] casts +/// it from the faces the solids drawn kept. +fn cast_parts(n: &Node) -> [(Vec, String); 2] { + let (_, fx) = solid(&n.appearance.effects).unwrap(); + let shapes = if is_container(n) { + members(n, "").unwrap() + } else { + let (path, rule) = profile(n).unwrap(); + vec![Profile { path, rule, items: solid_paint_sources(n).1, opacity: 1.0, scopes: vec![] }] + }; + let p = crate::merged_params(&fx.id, &fx.params); + let (_, built) = surfaces_for(None, n, fx, &shapes, true).unwrap(); + [cast_shadow(&fx.id, &p, &shapes), cast_built(&fx.id, &p, &shapes, built)].map(|c| { + let (caster, settings, cast) = c.unwrap().unwrap(); + let inks = cast.iter().map(|(_, ink, _)| ink.opacity).collect(); + (inks, format!("{:?}", shadow::scoped_art(n.id, &caster, settings, cast, has_masks(&shapes)))) + }) +} + +/// A fill, a stroke or a member whose opacity is NaN or infinite casts no part of the shadow, as one +/// at 0 % does, whether the shadow is cast from solids built for it or from the solids drawn; the +/// art beside it still casts its part. +#[test] +fn art_whose_opacity_is_not_a_finite_number_casts_no_shadow() { + // The art with opacity `opacity` in one place: a square's stroke, the fill of a group's second + // member, and a group's second member. + let objects = |opacity: f32| { + let mut fill = square_node(2, 200.0); + for item in &mut fill.appearance.items { + if let AppearanceItem::Fill(f) = item { + f.opacity = opacity; + } + } + let mut member = square_node(2, 200.0); + member.opacity = opacity; + [ + stroked_square(1, 0.0, opacity), + Node::group(NodeId(9), vec![Arc::new(square_node(1, 0.0)), Arc::new(fill)]), + Node::group(NodeId(9), vec![Arc::new(square_node(1, 0.0)), Arc::new(member)]), + ] + }; + for id in [EXTRUDE, INFLATE, ROTATE] { + for bad in [f32::NAN, f32::INFINITY] { + for (i, (mut n, mut clear)) in objects(bad).into_iter().zip(objects(0.0)).enumerate() { + n.appearance.effects.push(crate::new_effect(id, &json!({"shadow": true})).unwrap()); + clear.appearance.effects.push(crate::new_effect(id, &json!({"shadow": true})).unwrap()); + let expected = cast_parts(&clear); + for (k, (inks, shadow)) in cast_parts(&n).into_iter().enumerate() { + assert_eq!(inks, vec![1.0], "{id} {bad} object {i} cast {k}: only the opaque art casts"); + assert_eq!(shadow, expected[k].1, "{id} {bad} object {i} cast {k}"); + } + let mut opacities = vec![]; + art(&n, id).unwrap().walk(&mut |c| opacities.extend(shadow_opacity(c))); + assert_eq!(opacities, vec![0.5], "{id} {bad} object {i}"); + } + } + } +} diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index c6972468cc..f915c4bda6 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -585,6 +585,7 @@ since 2026-10-01. - 3D lighting presets: `crates/effects/src/lighting_presets.rs`, `crates/engine/src/cmd/effectcmd/lighting_presets_tests.rs` and the pointer tests in `crates/ui-egui/src/dialogs/revolve/tests.rs` cover recognition, atomic targeting, native round trips, preview, Cancel, reset and Undo. - 3D shadows: Revolve, Extrude & Bevel, Inflate and Rotate take a Shadows switch with Position (Behind Object or Below Object), Distance from Object (0–100 %) and Shadow Bounds (10–400 %) on the Lighting tab ([three-d.md](three-d.md#shadows)); the first light casts one hard-edged vector shadow, drawn behind the solids, in exports and kept by Expand Appearance. Evidence: `crates/three-d/src/shadow/tests.rs`, `crates/effects/src/solids/tests_shadow.rs`, `effectcmd::tests::a_3d_shadow_is_one_undo_step_saved_exported_and_expanded`. Softness and the shadows of lights after the first are not built. Scores unchanged. - 3D shadows: with Shadows on, Revolve, Extrude & Bevel, Inflate and Rotate cast the shadow from the faces kept by the build that draws each solid, so drawing an object builds each of its solids once. The art and the shadow are unchanged. Evidence: `crates/three-d/src/shadow/tests.rs` (`a_solid_and_its_shadow_are_built_once` and the tests that compare each solid and its shadow with the two built apart), `solids::tests_shadow::a_shadowed_3d_object_builds_each_solid_once`, `solids::tests_shadow::the_shadow_cast_from_the_solids_drawn_is_the_shadow_of_solids_built_for_it`, `revolve::tests::the_shadow_cast_from_the_surfaces_drawn_is_the_shadow_of_surfaces_swept_for_it`. Scores unchanged. +- 3D shadows: in Extrude & Bevel, Inflate and Rotate, a fill, stroke or member whose opacity is NaN or infinite casts no part of the shadow, as one at 0 % does, whether the shadow is cast from the solids drawn or from solids built for it alone; the art beside it still casts. This is VectorCraft's own rule. Evidence: `solids::tests_shadow::art_whose_opacity_is_not_a_finite_number_casts_no_shadow`. Scores unchanged. - Window › 3D and Materials panel: `crates/ui-egui/src/panels/three_d.rs` applies a 3D effect by object type (Plane, Extrude, Revolve, Inflate), changes an applied one in place (`effect.setParams` `threeDType`) and edits it with the options dialog's Object, Rotation and Lighting controls, one undo step per change; `crates/engine/src/cmd/effectcmd/three_d_type_tests.rs` and `crates/ui-egui/src/panels/tests_three_d.rs` cover it. No Materials tab. - 3D and Materials › Quick Actions › Expand as Wireframes (#6354): `effect.expandAsWireframes` replaces each selected 3D object with a group of 0.5 pt black stroked paths, the edges of the faces its effect draws, each edge once and joined end to end, curves kept, in one undo step ([three-d.md](three-d.md#3d-and-materials-panel)); `crates/effects/src/solids/wireframe/tests.rs`, `crates/engine/src/cmd/wireframe/tests.rs` and `crates/ui-egui/src/panels/tests_three_d.rs` cover it. Scores and hours unchanged. - 3D effects on clipping groups, blends and symbol instances (#6140, #5725): every 3D type builds from the art the object shows (the art inside the clipping path, the blend's keys and steps, the symbol's art where the instance places it), alone or in a group, on the canvas, in exports, Object › Rasterize and `assets.add3D`; an image is refused with a pointer to Object › Image Trace › Make. `crates/effects/src/solids/tests_sources.rs`, `crates/engine/src/cmd/effectcmd/three_d_source_tests.rs` and `fx::tests::a_symbol_instance_a_blend_and_a_clipping_group_draw_their_solids` cover it. Scores and estimates unchanged. From 18c98141d5da1d23b4d941316beede106502b7d7 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 14 Nov 2026 18:56:28 -0800 Subject: [PATCH 2/3] 3D: Revolve art at NaN or infinite opacity casts no shadow Revolve's shadow check, its kept-faces test, the single-object shadow and the group shadow now use the same rule as Extrude & Bevel, Inflate and Rotate: art whose opacity is not a finite number above zero casts nothing. Before, a NaN opacity passed the skip checks and gave the shadow fill a NaN opacity. --- crates/effects/src/revolve.rs | 62 ++++++++++++++++++++++++++--- crates/effects/src/solids/shadow.rs | 11 +++-- docs/target-app-parity.md | 2 +- 3 files changed, 65 insertions(+), 10 deletions(-) diff --git a/crates/effects/src/revolve.rs b/crates/effects/src/revolve.rs index a9c9482d1d..4d55664a67 100644 --- a/crates/effects/src/revolve.rs +++ b/crates/effects/src/revolve.rs @@ -166,7 +166,7 @@ pub(crate) fn validate_in(doc: Option<&Document>, n: &Node, id: &str, p: &Value) } } } - let casting = shapes.iter().filter(|s| surface_item(&s.items).is_some_and(|i| paint_of(i).1 * s.opacity > 0.0)); + let casting = shapes.iter().filter(|s| surface_item(&s.items).is_some_and(|i| crate::solids::shadow::casts(paint_of(i).1 * s.opacity))); return check_shadow(p, casting.map(|s| &s.path), b, options); } let (path, _) = object_profile(n, "Revolve")?; @@ -336,7 +336,8 @@ fn geometry(path: &PathData, fx: &[Effect], ctx: &GeomContext) -> PathData { /// The shadow of Revolve `fx` cast by the surface of `path` in `item`'s paint about the axis of /// `b`, revolved with `options`, as dark as `item` is opaque and in its blending mode; `None` with -/// the shadow off, when `item` is fully transparent or when it casts nothing, and an error when +/// the shadow off, when `item` is fully transparent or its opacity is not a finite number, or when +/// it casts nothing, and an error when /// the surface it is cast from is past the limits. /// /// `kept` holds the faces the surface drawn kept for its shadow ([`surface`]); without them the @@ -352,7 +353,7 @@ fn shadow_of( ) -> Result, String> { let Some(settings) = crate::solids::shadow::options(&merged_params(&fx.id, &fx.params)) else { return Ok(None) }; let (_, opacity, blend) = paint_of(item); - if opacity <= 0.0 { + if !crate::solids::shadow::casts(opacity) { return Ok(None); } let mut caster = vectorcraft_three_d::ShadowCaster::new(); @@ -363,7 +364,7 @@ fn shadow_of( /// Does Revolve `fx` cast a shadow, so a surface in paint as opaque as `opacity` keeps the faces /// its shadow is cast from ([`surface`])? fn casts(fx: &Effect, opacity: f32) -> bool { - opacity > 0.0 && crate::solids::shadow::options(&merged_params(&fx.id, &fx.params)).is_some() + crate::solids::shadow::casts(opacity) && crate::solids::shadow::options(&merged_params(&fx.id, &fx.params)).is_some() } /// Add the surface of `path` about the axis of `b` to `caster`: the faces it kept when it was @@ -406,9 +407,9 @@ fn group_shadow( for s in shapes { let kept = kept.next().flatten(); let Some((_, opacity, blend)) = surface_item(&s.items).map(paint_of) else { continue }; - // A member whose surface is fully transparent casts nothing. + // A member whose surface is fully transparent, or whose opacity is not a finite number, casts nothing. let ink = crate::solids::shadow::Ink { opacity: opacity * s.opacity, blend }; - if ink.opacity <= 0.0 { + if !ink.casts() { continue; } let faces = cast_surface(&mut caster, &s.path, b, options, kept)?; @@ -1810,6 +1811,55 @@ mod tests { } } } + + /// A surface whose fill, or whose member, is at NaN or infinite opacity casts no shadow, as one + /// at 0 % does, whether the shadow is cast from the surface drawn or swept for it alone; the + /// member beside it still casts its part. + #[test] + fn a_surface_whose_opacity_is_not_a_finite_number_casts_no_shadow() { + let fx = crate::new_effect(REVOLVE, &json!({"shadow": true})).unwrap(); + let group = |opacity: f32| { + let mut other = profile_node(); + other.id = NodeId(2); + other.transform(Affine::translate((120.0, 0.0)), false); + other.opacity = opacity; + let mut g = Node::group(NodeId(9), vec![Arc::new(profile_node()), Arc::new(other)]); + g.appearance.effects.push(fx.clone()); + g + }; + let group_shadows = |g: &Node| { + let shapes = members(g, "").unwrap(); + let b = union_bounds(&shapes).unwrap(); + let options = shared_options(&shapes, fx_options(&fx)).unwrap(); + let kept: Vec> = shapes + .iter() + .map(|s| { + let (paint, opacity, blend) = paint_of(surface_item(&s.items).unwrap()); + let cast = casts(&fx, opacity * s.opacity); + surface(None, g, &s.path, b, options, &fx, paint, opacity, blend, false, &mut Copies::default(), cast).unwrap().2 + }) + .collect(); + [group_shadow(g.id, &fx, &shapes, b, options, kept).unwrap(), group_shadow(g.id, &fx, &shapes, b, options, vec![]).unwrap()] + .map(|art| format!("{art:?}")) + }; + let expected = group_shadows(&group(0.0)); + for bad in [f32::NAN, f32::INFINITY] { + let mut n = profile_node(); + if let AppearanceItem::Fill(f) = &mut n.appearance.items[0] { + f.opacity = bad; + } + let (path, _) = profile(&n).unwrap(); + let b = path.bounds().unwrap(); + let item = surface_item(&n.appearance.items).unwrap(); + assert!(!casts(&fx, bad), "{bad}: the surface keeps no faces for a shadow"); + let (_, kept) = object_surface(None, &n, &path, &fx, item, false, &mut Copies::default()).unwrap(); + assert_eq!(shadow_of(n.id, &fx, &path, item, b, fx_options(&fx), kept).unwrap(), None, "{bad} drawn"); + assert_eq!(shadow_of(n.id, &fx, &path, item, b, fx_options(&fx), None).unwrap(), None, "{bad} alone"); + let shadows = group_shadows(&group(bad)); + assert!(!expected[0].is_empty() && expected[0] != "[]", "the opaque member casts"); + assert_eq!(shadows, expected, "{bad}: only the opaque member casts"); + } + } } #[cfg(test)] diff --git a/crates/effects/src/solids/shadow.rs b/crates/effects/src/solids/shadow.rs index 15a8e9d3c1..81542a3722 100644 --- a/crates/effects/src/solids/shadow.rs +++ b/crates/effects/src/solids/shadow.rs @@ -51,13 +51,18 @@ pub(crate) struct Ink { } impl Ink { - /// Whether art in this ink casts part of a shadow: its opacity is a finite number above zero. - /// Art at zero, negative, NaN or infinite opacity casts nothing. + /// Whether art in this ink casts part of a shadow ([`casts`]). pub(crate) fn casts(self) -> bool { - self.opacity.is_finite() && self.opacity > 0.0 + casts(self.opacity) } } +/// Whether art at `opacity` casts part of a shadow: its opacity is a finite number above zero. +/// Art at zero, negative, NaN or infinite opacity casts nothing. +pub(crate) fn casts(opacity: f32) -> bool { + opacity.is_finite() && opacity > 0.0 +} + /// The shadow of the faces in `caster` as a path of object `id`, cast by art in `ink`: filled /// black at [`SHADOW_OPACITY`] times its opacity, in its blending mode, with the nonzero rule; /// `None` when it casts nothing. diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 4ea5de5557..ebb85a6acf 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -585,7 +585,7 @@ since 2026-10-01. - 3D lighting presets: `crates/effects/src/lighting_presets.rs`, `crates/engine/src/cmd/effectcmd/lighting_presets_tests.rs` and the pointer tests in `crates/ui-egui/src/dialogs/revolve/tests.rs` cover recognition, atomic targeting, native round trips, preview, Cancel, reset and Undo. - 3D shadows: Revolve, Extrude & Bevel, Inflate and Rotate take a Shadows switch with Position (Behind Object or Below Object), Distance from Object (0–100 %) and Shadow Bounds (10–400 %) on the Lighting tab ([three-d.md](three-d.md#shadows)); the first light casts one hard-edged vector shadow, drawn behind the solids, in exports and kept by Expand Appearance. Evidence: `crates/three-d/src/shadow/tests.rs`, `crates/effects/src/solids/tests_shadow.rs`, `effectcmd::tests::a_3d_shadow_is_one_undo_step_saved_exported_and_expanded`. Softness and the shadows of lights after the first are not built. Scores unchanged. - 3D shadows: with Shadows on, Revolve, Extrude & Bevel, Inflate and Rotate cast the shadow from the faces kept by the build that draws each solid, so drawing an object builds each of its solids once. The art and the shadow are unchanged. Evidence: `crates/three-d/src/shadow/tests.rs` (`a_solid_and_its_shadow_are_built_once` and the tests that compare each solid and its shadow with the two built apart), `solids::tests_shadow::a_shadowed_3d_object_builds_each_solid_once`, `solids::tests_shadow::the_shadow_cast_from_the_solids_drawn_is_the_shadow_of_solids_built_for_it`, `revolve::tests::the_shadow_cast_from_the_surfaces_drawn_is_the_shadow_of_surfaces_swept_for_it`. Scores unchanged. -- 3D shadows: in Extrude & Bevel, Inflate and Rotate, a fill, stroke or member whose opacity is NaN or infinite casts no part of the shadow, as one at 0 % does, whether the shadow is cast from the solids drawn or from solids built for it alone; the art beside it still casts. This is VectorCraft's own rule. Evidence: `solids::tests_shadow::art_whose_opacity_is_not_a_finite_number_casts_no_shadow`. Scores unchanged. +- 3D shadows: in Revolve, Extrude & Bevel, Inflate and Rotate, a fill, stroke or member whose opacity is NaN or infinite casts no part of the shadow, as one at 0 % does, whether the shadow is cast from the solids drawn or from solids built for it alone; the art beside it still casts. This is VectorCraft's own rule. Evidence: `solids::tests_shadow::art_whose_opacity_is_not_a_finite_number_casts_no_shadow`, `revolve::tests::a_surface_whose_opacity_is_not_a_finite_number_casts_no_shadow`. Scores unchanged. - Window › 3D and Materials panel: `crates/ui-egui/src/panels/three_d.rs` applies a 3D effect by object type (Plane, Extrude, Revolve, Inflate), changes an applied one in place (`effect.setParams` `threeDType`) and edits it with the options dialog's Object, Rotation and Lighting controls, one undo step per change; `crates/engine/src/cmd/effectcmd/three_d_type_tests.rs` and `crates/ui-egui/src/panels/tests_three_d.rs` cover it. No Materials tab. - 3D and Materials › Quick Actions › Expand as Wireframes (#6354): `effect.expandAsWireframes` replaces each selected 3D object with a group of 0.5 pt black stroked paths, the edges of the faces its effect draws, each edge once and joined end to end, curves kept, in one undo step ([three-d.md](three-d.md#3d-and-materials-panel)); `crates/effects/src/solids/wireframe/tests.rs`, `crates/engine/src/cmd/wireframe/tests.rs` and `crates/ui-egui/src/panels/tests_three_d.rs` cover it. Scores and hours unchanged. - 3D effects on clipping groups, blends and symbol instances (#6140, #5725): every 3D type builds from the art the object shows (the art inside the clipping path, the blend's keys and steps, the symbol's art where the instance places it), alone or in a group, on the canvas, in exports, Object › Rasterize and `assets.add3D`; an image is refused with a pointer to Object › Image Trace › Make. `crates/effects/src/solids/tests_sources.rs`, `crates/engine/src/cmd/effectcmd/three_d_source_tests.rs` and `fx::tests::a_symbol_instance_a_blend_and_a_clipping_group_draw_their_solids` cover it. Scores and estimates unchanged. From 2d00e6f3972bec337da25d5ab2768db8817e77c0 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sun, 15 Nov 2026 01:46:57 -0800 Subject: [PATCH 3/3] Revolve: shadow_of's doc comment wraps at the line width --- crates/effects/src/revolve.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/effects/src/revolve.rs b/crates/effects/src/revolve.rs index a7373c7e88..a49e817ef2 100644 --- a/crates/effects/src/revolve.rs +++ b/crates/effects/src/revolve.rs @@ -351,8 +351,7 @@ fn geometry(path: &PathData, fx: &[Effect], ctx: &GeomContext) -> PathData { /// The shadow of Revolve `fx` cast by the surface of `path` in `item`'s paint about the axis of /// `b`, revolved with `options`, as dark as `item` is opaque and in its blending mode; `None` with /// the shadow off, when `item` is fully transparent or its opacity is not a finite number, or when -/// it casts nothing, and an error when -/// the surface it is cast from is past the limits. +/// it casts nothing, and an error when the surface it is cast from is past the limits. /// /// `kept` holds the faces the surface drawn kept for its shadow ([`surface`]); without them the /// surface is swept again for its shadow alone.