mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-28 11:47:21 -06:00
222 lines
9.3 KiB
Rust
222 lines
9.3 KiB
Rust
use super::*;
|
|
|
|
/// A closed contour smaller than this (in square document units) has no usable winding direction,
|
|
/// so Make Compound Path leaves its orientation alone rather than flipping it on noise.
|
|
const MIN_WINDING_AREA: f64 = 1e-6;
|
|
|
|
pub(super) fn compound_make(s: &mut Session, _: &Value) -> Result<Value> {
|
|
let ids = selected_roots(s)?;
|
|
let Some(backmost) = ids.first().copied() else { return Err(EngineError::Other("nothing selected".into())) };
|
|
let id = s.edit("Make Compound Path", |d, sel| {
|
|
d.node(backmost).ok_or(EngineError::NoNode(backmost))?;
|
|
let mut units: Vec<Vec<Arc<Node>>> = vec![];
|
|
let mut appearance = None;
|
|
for id in &ids {
|
|
let n = d.node(*id).ok_or(EngineError::NoNode(*id))?;
|
|
if !matches!(n.kind, NodeKind::Path { .. } | NodeKind::Compound { .. } | NodeKind::Group { .. }) {
|
|
return Err(EngineError::Other("compound paths can only contain paths".into()));
|
|
}
|
|
compound_members(n, &mut appearance, &mut units);
|
|
}
|
|
let cid = d.alloc_id();
|
|
let mut c = Node::new(cid, NodeKind::Compound { children: vec![], rule: FillRule::NonZero });
|
|
c.appearance = appearance.unwrap_or_default();
|
|
let ch = c.children_mut().ok_or(EngineError::NoNode(cid))?;
|
|
// The paths in front run against the backmost one, so where they overlap it they cut holes
|
|
// under the non-zero fill rule (#1197). A compound path's paths turn together, by its
|
|
// first: its own holes (a letter's counters) stay holes.
|
|
let direction = |n: &Node| {
|
|
n.path_data()
|
|
.and_then(|pd| pd.subpaths.first())
|
|
.filter(|sp| sp.closed && sp.signed_area().abs() > MIN_WINDING_AREA)
|
|
.map_or(0.0, |sp| sp.signed_area().signum())
|
|
};
|
|
let first = |unit: &[Arc<Node>]| unit.first().map_or(0.0, |p| direction(p));
|
|
let back = units.first().map_or(0.0, |u| first(u));
|
|
for (i, unit) in units.into_iter().enumerate() {
|
|
let flip = i > 0 && back != 0.0 && first(&unit) == back;
|
|
for mut p in unit {
|
|
let pn = Arc::make_mut(&mut p);
|
|
pn.appearance = Appearance::default().into();
|
|
if let NodeKind::Path { path, live, .. } = &mut pn.kind {
|
|
*live = None;
|
|
if flip {
|
|
path.reverse();
|
|
}
|
|
}
|
|
ch.push(p);
|
|
}
|
|
}
|
|
d.wrap(&ids, |_| c)?;
|
|
sel.set([cid]);
|
|
Ok(cid)
|
|
})?;
|
|
Ok(json!({ "id": id.0 }))
|
|
}
|
|
|
|
/// The paths in `n` (a path, a compound path, or a group searched in depth) onto `out`, in paint
|
|
/// order: each path alone, a compound path's paths together. Into `paint` goes the appearance of
|
|
/// the backmost found so far: the path's own, or its compound path's, which is what painted it. A
|
|
/// group has no paint of its own to give: a group of outlined type made a compound path that drew
|
|
/// nothing.
|
|
fn compound_members(n: &Node, paint: &mut Option<vectorcraft_doc::Shared<Appearance>>, out: &mut Vec<Vec<Arc<Node>>>) {
|
|
match (&n.kind, n.children()) {
|
|
(NodeKind::Path { .. }, _) => out.push(vec![Arc::new(n.clone())]),
|
|
(NodeKind::Compound { .. }, Some(paths)) if !paths.is_empty() => out.push(paths.clone()),
|
|
(_, children) => return children.into_iter().flatten().for_each(|c| compound_members(c, paint, out)),
|
|
}
|
|
paint.get_or_insert_with(|| n.appearance.clone());
|
|
}
|
|
|
|
/// Is `n` a compound path to release: a compound path, or a path of several subpaths (an SVG `d`
|
|
/// with several `M`s, a PDF path), which is one in Illustrator too (#1163)?
|
|
fn releases(n: &Node) -> bool {
|
|
match &n.kind {
|
|
NodeKind::Compound { .. } => true,
|
|
NodeKind::Path { path, .. } => path.subpaths.len() > 1,
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
pub(super) fn compound_release(s: &mut Session, _: &Value) -> Result<Value> {
|
|
let ids = selected_roots(s)?;
|
|
// With no compound path selected, the document, the selection and the undo history stay as
|
|
// they are.
|
|
let d = &s.doc()?.doc;
|
|
if !ids.iter().any(|id| d.node(*id).is_some_and(releases)) {
|
|
return ok();
|
|
}
|
|
s.edit("Release Compound Path", |d, sel| {
|
|
let mut out = vec![];
|
|
let mut released = vec![];
|
|
for id in &ids {
|
|
let Some(n) = d.node(*id).cloned().filter(releases) else { continue };
|
|
let mut parts = vec![];
|
|
match &n.kind {
|
|
NodeKind::Compound { children, .. } => {
|
|
for c in children {
|
|
let mut c = (**c).clone();
|
|
c.appearance = n.appearance.clone();
|
|
out.push(c.id);
|
|
parts.push(c);
|
|
}
|
|
}
|
|
// Each subpath becomes a path of its own with the path's paint, in the same
|
|
// stacking order; the first keeps the path's id.
|
|
NodeKind::Path { path, .. } => {
|
|
for (k, sp) in path.subpaths.iter().enumerate() {
|
|
let mut c = if k == 0 { n.clone() } else { d.reid(&n) };
|
|
if let NodeKind::Path { path, live, .. } = &mut c.kind {
|
|
(*path, *live) = (PathData::single(sp.clone()), None);
|
|
}
|
|
out.push(c.id);
|
|
parts.push(c);
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
released.push((*id, parts));
|
|
}
|
|
d.insert_beside(released, true)?;
|
|
sel.set(out);
|
|
Ok(())
|
|
})?;
|
|
ok()
|
|
}
|
|
|
|
/// Make `top` (a path, compound path or text object) the clipping path: it loses its paint (it
|
|
/// stays unpainted after Release).
|
|
pub(in super::super) fn make_clipping_path(d: &mut Document, top: NodeId) -> Result<()> {
|
|
as_clipping_path(d.node_mut(top).ok_or(EngineError::NoNode(top))?)
|
|
}
|
|
|
|
/// Turn path, compound path or text `c` into a clipping path (its paint goes).
|
|
pub(in super::super) fn as_clipping_path(c: &mut Node) -> Result<()> {
|
|
if !matches!(c.kind, NodeKind::Path { guide: false, .. } | NodeKind::Compound { .. } | NodeKind::Text(_)) {
|
|
return Err(EngineError::Other("the top object must be a path, compound path or text object to use as a clipping mask".into()));
|
|
}
|
|
c.appearance = Appearance::basic(Paint::None, Paint::None, 0.0).into();
|
|
match &mut c.kind {
|
|
NodeKind::Path { clipping, .. } => *clipping = true,
|
|
NodeKind::Text(t) => {
|
|
for r in &mut t.runs {
|
|
(r.style.fill, r.style.stroke) = (Paint::None, Paint::None);
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Stop clip group or clipped layer `id` clipping; its clipping path stays, with its paint.
|
|
pub(in super::super) fn release_clip(d: &mut Document, id: NodeId) {
|
|
let Some(n) = d.node_mut(id).filter(|n| n.clips()) else { return };
|
|
n.set_clips(false);
|
|
let clip = n.children().and_then(|c| c.first()).map(|c| c.id);
|
|
if let Some(c) = clip.and_then(|c| d.node_mut(c))
|
|
&& let NodeKind::Path { clipping, .. } = &mut c.kind
|
|
{
|
|
*clipping = false;
|
|
}
|
|
}
|
|
|
|
pub(super) fn clip_make(s: &mut Session, _: &Value) -> Result<Value> {
|
|
let ids = selected_roots(s)?;
|
|
let Some(&top) = ids.last() else { return Err(EngineError::Other("nothing selected".into())) };
|
|
let gid = s.edit("Make Clipping Mask", |d, sel| {
|
|
make_clipping_path(d, top)?;
|
|
let gid = d.alloc_id();
|
|
// The topmost object, the clipping path, goes first, and the others follow in paint order.
|
|
d.wrap(&ids, |mut children| {
|
|
children.rotate_right(1);
|
|
Node::new(gid, NodeKind::Group { children, clip: true })
|
|
})?;
|
|
sel.set([gid]);
|
|
Ok(gid)
|
|
})?;
|
|
Ok(json!({ "id": gid.0 }))
|
|
}
|
|
|
|
pub(super) fn clip_release(s: &mut Session, _: &Value) -> Result<Value> {
|
|
let ids = selected_roots(s)?;
|
|
// With no clip group selected, the document and its undo history stay as they are.
|
|
let d = &s.doc()?.doc;
|
|
if !ids.iter().any(|id| d.node(*id).is_some_and(Node::clips)) {
|
|
return ok();
|
|
}
|
|
s.edit("Release Clipping Mask", |d, _| {
|
|
for id in &ids {
|
|
release_clip(d, *id);
|
|
}
|
|
Ok(())
|
|
})?;
|
|
ok()
|
|
}
|
|
|
|
/// Selects the clipping path (`mask`) or the clipped contents of every selected clip group (or of the
|
|
/// clip group containing a selected object), like Illustrator's Edit Contents / Edit Clipping Path toggle.
|
|
pub(super) fn clip_edit(s: &mut Session, mask: bool) -> Result<Value> {
|
|
let st = s.doc()?;
|
|
let mut ids = vec![];
|
|
for id in st.selection.objects.iter().copied() {
|
|
let mut cur = Some(id);
|
|
while let Some(c) = cur {
|
|
if let Some(NodeKind::Group { children, clip: true }) = st.doc.node(c).map(|n| &n.kind) {
|
|
if mask {
|
|
ids.extend(children.first().map(|c| c.id));
|
|
} else {
|
|
ids.extend(children.iter().skip(1).map(|c| c.id));
|
|
}
|
|
break;
|
|
}
|
|
cur = st.doc.position(c).and_then(|(par, _, _)| par);
|
|
}
|
|
}
|
|
ids.dedup();
|
|
if ids.is_empty() {
|
|
return Err(EngineError::Other("select a clip group".into()));
|
|
}
|
|
s.select(|_, sel| sel.set(ids.iter().copied()))?;
|
|
Ok(json!({ "count": ids.len() }))
|
|
}
|