mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-12 08:52:26 +00:00
3D: Rotate keeps each clipped fill in its place among the strokes
Inside a clipping group, the fills of a shape were cut into one piece in the top fill's place. Rotate turns every fill, so with a fill, a stroke and a fill, the lower fill was drawn above the stroke. With Rotate, each fill now becomes its own piece in its own place. Extrude & Bevel, Inflate and Revolve build from the top fill and keep one piece there, also when an object changes from Rotate to another 3D type.
This commit is contained in:
@@ -7,7 +7,7 @@ use vectorcraft_doc::{Appearance, AppearanceItem, Document, Effect, FillLayer, L
|
||||
use vectorcraft_geom::{PathData, PathEl, Point, Rect, Vec2};
|
||||
use vectorcraft_three_d::{Edge, Revolve};
|
||||
|
||||
use crate::solids::{Profile, Scope, is_container, members, object_profile, profile, reshape_members, resolve, solid_paints, union_bounds};
|
||||
use crate::solids::{Profile, Scope, is_container, members, object_profile, profile, reshape_members, resolve_for, solid_paints, union_bounds};
|
||||
use crate::util::{flag, num, text};
|
||||
use crate::{GeomContext, apply_geometry_with, is_geometry, merged_params};
|
||||
|
||||
@@ -79,10 +79,10 @@ pub fn validate_revolve(n: &Node, p: &Value) -> Result<(), String> {
|
||||
}
|
||||
|
||||
/// [`validate_revolve`] with the symbols of `doc`: a symbol instance, a blend and a clipping group
|
||||
/// revolve the art they show ([`resolve`]).
|
||||
/// revolve the art they show ([`resolve_for`]), cut for Revolve whatever 3D effect `n` carries now.
|
||||
pub fn validate_revolve_in(doc: Option<&Document>, n: &Node, p: &Value) -> Result<(), String> {
|
||||
validate_revolve_params(p)?;
|
||||
let n = &*resolve(doc, n, "Revolve")?;
|
||||
let n = &*resolve_for(doc, n, "Revolve", false)?;
|
||||
let effects = &n.appearance.effects;
|
||||
// The profile is measured before the object's one Revolve, hidden or shown.
|
||||
let before = effects.iter().position(|e| e.id == REVOLVE).and_then(|i| effects.get(..i)).unwrap_or(effects);
|
||||
@@ -329,7 +329,7 @@ pub fn revolve_art(n: &Node) -> Option<Node> {
|
||||
}
|
||||
|
||||
/// [`revolve_art`] with the symbols of `doc`: a symbol instance, a blend and a clipping group, alone
|
||||
/// or in a group, revolve the art they show ([`resolve`]). `None` too when that art can't be
|
||||
/// or in a group, revolve the art they show ([`resolve_for`]). `None` too when that art can't be
|
||||
/// resolved.
|
||||
pub fn revolve_art_in(doc: Option<&Document>, n: &Node) -> Option<Node> {
|
||||
art(doc, n, false)
|
||||
@@ -413,7 +413,7 @@ fn art(doc: Option<&Document>, n: &Node, expand: bool) -> Option<Node> {
|
||||
if !has_revolve(n) {
|
||||
return None;
|
||||
}
|
||||
let n = &*resolve(doc, n, "Revolve").ok()?;
|
||||
let n = &*resolve_for(doc, n, "Revolve", false).ok()?;
|
||||
if is_container(n) {
|
||||
return group_art(n, expand);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ mod classic;
|
||||
pub use classic::{CLASSIC_EXTRUDE, SHADING_COLORS, classic_extrude_options, classic_shading};
|
||||
|
||||
mod source;
|
||||
pub(crate) use source::resolve;
|
||||
pub(crate) use source::{resolve, resolve_for};
|
||||
|
||||
pub const EXTRUDE: &str = "threeD.extrude";
|
||||
pub const INFLATE: &str = "threeD.inflate";
|
||||
@@ -216,7 +216,7 @@ pub fn validate_solid(n: &Node, id: &str, p: &Value) -> Result<(), String> {
|
||||
pub fn validate_solid_in(doc: Option<&Document>, n: &Node, id: &str, p: &Value) -> Result<(), String> {
|
||||
validate_solid_params(id, p)?;
|
||||
let label = label_of(id);
|
||||
let n = &*resolve(doc, n, label)?;
|
||||
let n = &*source::resolve_for(doc, n, label, id == ROTATE)?;
|
||||
if is_container(n) {
|
||||
let shapes = members(n, label)?;
|
||||
let b = union_bounds(&shapes).ok_or_else(|| format!("{label} requires a group holding a shape with at least two anchors"))?;
|
||||
|
||||
@@ -52,12 +52,21 @@ fn needs(n: &Node) -> bool {
|
||||
/// is a symbol instance, a blend or a clipping group, otherwise a group with `n`'s id, appearance
|
||||
/// and transparency holding that art (a symbol instance's symbol art from `doc`). An error names
|
||||
/// what can't be resolved: an image, a missing symbol, art nested or numbering past the limits, or
|
||||
/// a clipping path too complex to cut the art with. `label` is the effect's name.
|
||||
/// a clipping path too complex to cut the art with. `label` is the effect's name. The art is cut
|
||||
/// for the solid effect `n` carries ([`resolve_for`]).
|
||||
pub(crate) fn resolve<'a>(doc: Option<&Document>, n: &'a Node, label: &str) -> Result<Cow<'a, Node>, String> {
|
||||
let every_fill = super::solid(&n.appearance.effects).is_some_and(|(_, fx)| fx.id == super::ROTATE);
|
||||
resolve_for(doc, n, label, every_fill)
|
||||
}
|
||||
|
||||
/// [`resolve`] for the 3D effect that builds from `n`: `every_fill` for Rotate, which turns every
|
||||
/// fill, so each fill a clipping path cuts keeps its own place among the strokes. Otherwise the
|
||||
/// effect builds from the top fill, and the fills cut go together in the top fill's place.
|
||||
pub(crate) fn resolve_for<'a>(doc: Option<&Document>, n: &'a Node, label: &str, every_fill: bool) -> Result<Cow<'a, Node>, String> {
|
||||
if !needs(n) {
|
||||
return Ok(Cow::Borrowed(n));
|
||||
}
|
||||
let mut r = Resolver { doc, label, leaves: 0, work: 0, shown: None };
|
||||
let mut r = Resolver { doc, label, every_fill, leaves: 0, work: 0, shown: None };
|
||||
let children = r.content(n, 0);
|
||||
#[cfg(test)]
|
||||
WORK.with(|w| w.set(r.work));
|
||||
@@ -75,6 +84,8 @@ thread_local! {
|
||||
struct Resolver<'a> {
|
||||
doc: Option<&'a Document>,
|
||||
label: &'a str,
|
||||
/// Does each fill a clipping path cuts become a piece of its own ([`resolve_for`])?
|
||||
every_fill: bool,
|
||||
/// The objects other than groups and layers kept so far, counted against [`MAX_MEMBERS`].
|
||||
leaves: usize,
|
||||
/// The objects visited or made so far, counted against [`MAX_WORK`].
|
||||
@@ -268,10 +279,13 @@ impl Resolver<'_> {
|
||||
let Some((path, rule)) = profile(n) else { return Ok(Some(n.clone())) };
|
||||
let items = solid_paints(n);
|
||||
// The fills' piece goes where the top fill is, the one Extrude & Bevel and Inflate build
|
||||
// their solid from, so the strokes above and below it keep their places.
|
||||
// their solid from, so the strokes above and below it keep their places. Rotate turns
|
||||
// every fill, so each fill is a piece in its own place.
|
||||
let top_fill = items.iter().rposition(|i| matches!(i, AppearanceItem::Fill(_)));
|
||||
let fill_pieces =
|
||||
if self.every_fill { items.iter().filter(|i| matches!(i, AppearanceItem::Fill(_))).count() } else { usize::from(top_fill.is_some()) };
|
||||
// Each cut makes a piece, counted before it is made: the shape was counted once already.
|
||||
let made = usize::from(top_fill.is_some()) + items.iter().filter(|i| matches!(i, AppearanceItem::Stroke(_))).count();
|
||||
let made = fill_pieces + items.iter().filter(|i| matches!(i, AppearanceItem::Stroke(_))).count();
|
||||
self.spend(made)?;
|
||||
self.leaves = self.leaves.saturating_add(made.saturating_sub(1));
|
||||
if self.leaves > MAX_MEMBERS {
|
||||
@@ -284,12 +298,19 @@ impl Resolver<'_> {
|
||||
Ok(inside.bounds().is_some().then_some(inside))
|
||||
};
|
||||
let fills: Vec<AppearanceItem> = items.iter().filter(|i| matches!(i, AppearanceItem::Fill(_))).cloned().collect();
|
||||
// Every fill paints the same area: it is cut once.
|
||||
let mut fill_area: Option<Option<PathData>> = None;
|
||||
let mut pieces = vec![];
|
||||
for (k, item) in items.iter().enumerate() {
|
||||
match item {
|
||||
AppearanceItem::Fill(_) if Some(k) == top_fill => {
|
||||
if let Some(area) = cut(&path, rule)? {
|
||||
pieces.push(Arc::new(shape(n, area, FillRule::NonZero, fills.clone())));
|
||||
AppearanceItem::Fill(_) if self.every_fill || Some(k) == top_fill => {
|
||||
let area = match &fill_area {
|
||||
Some(area) => area.clone(),
|
||||
None => fill_area.insert(cut(&path, rule)?).clone(),
|
||||
};
|
||||
if let Some(area) = area {
|
||||
let painted = if self.every_fill { vec![item.clone()] } else { fills.clone() };
|
||||
pieces.push(Arc::new(shape(n, area, FillRule::NonZero, painted)));
|
||||
}
|
||||
}
|
||||
AppearanceItem::Fill(_) => {}
|
||||
|
||||
@@ -146,7 +146,24 @@ fn a_clipping_groups_solid_is_built_from_its_art_inside_the_clip() {
|
||||
let colors = |n: &Node| three_d_meshes_in(Some(&d), &with(n.clone(), EXTRUDE, json!({}))).unwrap().iter().map(|m| m.color).collect::<Vec<_>>();
|
||||
let plain = colors(&Node::group(NodeId(10), vec![Arc::new(layered.clone())]));
|
||||
assert_eq!(plain.len(), 2, "the stroke's solid and the top fill's");
|
||||
assert_eq!(colors(&clip_group(10, Rect::new(0.0, 0.0, 400.0, 400.0), layered)), plain);
|
||||
assert_eq!(colors(&clip_group(10, Rect::new(0.0, 0.0, 400.0, 400.0), layered.clone())), plain);
|
||||
// Rotate turns both fills and the stroke, each in its own place.
|
||||
let turned = |n: &Node| three_d_meshes_in(Some(&d), &with(n.clone(), ROTATE, front())).unwrap().iter().map(|m| m.color).collect::<Vec<_>>();
|
||||
let plain = turned(&Node::group(NodeId(10), vec![Arc::new(layered.clone())]));
|
||||
assert_eq!(plain.len(), 3, "both fills' surfaces and the stroke's");
|
||||
let clipped = clip_group(10, Rect::new(0.0, 0.0, 400.0, 400.0), layered);
|
||||
assert_eq!(turned(&clipped), plain);
|
||||
assert_eq!(validate_three_d_in(Some(&d), &clipped, ROTATE, &json!({})), Ok(()));
|
||||
// Each fill Rotate turns counts against the limit on objects; another effect takes the top
|
||||
// fill only, whatever 3D effect the object carries before it changes type.
|
||||
let mut many = rect(12, 100.0, 100.0, 150.0, 200.0, 0.0);
|
||||
many.appearance =
|
||||
Appearance { items: vec![AppearanceItem::Fill(FillLayer::new(Paint::solid(Color::BLACK))); MAX_MEMBERS + 1], ..Default::default() }.into();
|
||||
let rotated = with(clip_group(10, Rect::new(0.0, 0.0, 400.0, 400.0), many), ROTATE, front());
|
||||
assert!(validate_three_d_in(Some(&d), &rotated, ROTATE, &json!({})).is_err_and(|e| e.contains("takes at most")));
|
||||
for id in [EXTRUDE, crate::REVOLVE] {
|
||||
assert_eq!(validate_three_d_in(Some(&d), &rotated, id, &json!({})), Ok(()), "Rotate changed to {id}");
|
||||
}
|
||||
// Art nested past the limit inside a clipping group is refused, not followed to any depth.
|
||||
let mut deep = rect(12, 100.0, 100.0, 200.0, 200.0, 0.0);
|
||||
for i in 0..200 {
|
||||
|
||||
Reference in New Issue
Block a user