Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions crates/oxvg/src/commands/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,18 @@ impl RunCommand for ActionList {
println!("# Path Intersect\n");
println!(include_str!("../spec/manipulate/path_intersect.md"));
}
if parts.is_empty() || parts.contains(PATH_UNION) {
println!("# Path Union\n");
println!(include_str!("../spec/manipulate/path_union.md"));
}
if parts.is_empty() || parts.contains(PATH_SUBTRACT) {
println!("# Path Subtract\n");
println!(include_str!("../spec/manipulate/path_subtract.md"));
}
if parts.is_empty() || parts.contains(PATH_XOR) {
println!("# Path Xor\n");
println!(include_str!("../spec/manipulate/path_xor.md"));
}
if parts.is_empty() || parts.contains(STYLE) {
println!("# Style\n");
println!(include_str!("../spec/manipulate/style.md"));
Expand Down Expand Up @@ -244,6 +256,9 @@ impl RunCommand for ActionList {
const ATTR: &str = "-attr";
const CLASS: &str = "-class";
const PATH_INTERSECT: &str = "-path-intersect";
const PATH_UNION: &str = "-path-union";
const PATH_SUBTRACT: &str = "-path-subtract";
const PATH_XOR: &str = "-path-xor";
const STYLE: &str = "-style";
const MATRIX: &str = "-matrix";
const TRANSLATE: &str = "-translate";
Expand Down Expand Up @@ -297,6 +312,9 @@ fn parse(command_list: Vec<String>) -> anyhow::Result<Vec<oxvg_actions::Action<'
},
CLASS => oxvg_actions::Action::Class(get_part(&mut parts)?),
PATH_INTERSECT => oxvg_actions::Action::PathIntersect,
PATH_UNION => oxvg_actions::Action::PathUnion,
PATH_SUBTRACT => oxvg_actions::Action::PathSubtract,
PATH_XOR => oxvg_actions::Action::PathXor,
STYLE => oxvg_actions::Action::Style {
property: get_part(&mut parts)?,
value: get_part(&mut parts)?,
Expand Down
15 changes: 15 additions & 0 deletions crates/oxvg_actions/src/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ pub enum Action<'input> {
Class(Atom<'input>),
/// See [`Actor::path_intersect`]
PathIntersect,
/// See [`Actor::path_union`]
PathUnion,
/// See [`Actor::path_subtract`]
PathSubtract,
/// See [`Actor::path_xor`]
PathXor,
/// See [`Actor::style`]
Style {
/// The CSS name of the property
Expand Down Expand Up @@ -85,6 +91,12 @@ pub enum ActionNapi {
Class(String),
/// See [`Actor::path_intersect`]
PathIntersect,
/// See [`Actor::path_union`]
PathUnion,
/// See [`Actor::path_subtract`]
PathSubtract,
/// See [`Actor::path_xor`]
PathXor,
/// See [`Actor::style`]
Style {
/// The CSS name of the property
Expand Down Expand Up @@ -165,6 +177,9 @@ impl<'input, 'arena> Actor<'input, 'arena> {
Action::Class(name) => return self.class(&name),
Action::Style { property, value } => return self.style(&property, &value),
Action::PathIntersect => return self.path_intersect(),
Action::PathUnion => return self.path_union(),
Action::PathSubtract => return self.path_subtract(),
Action::PathXor => return self.path_xor(),
Action::Matrix(a, b, c, d, e, f) => return self.matrix(a, b, c, d, e, f),
Action::Translate(x, y) => return self.translate(x, y),
Action::Scale(x, y) => return self.scale(x, y),
Expand Down
107 changes: 79 additions & 28 deletions crates/oxvg_actions/src/actions/manipulate/path.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
use lightningcss::values::shape::FillRule;
use oxvg_ast::{
element::Element,
get_attribute, get_computed_style, is_element, set_attribute,
get_attribute, get_computed_style, has_attribute, is_element, set_attribute,
style::{self, ComputedStyles},
};
use oxvg_collections::attribute::inheritable::Inheritable;
use oxvg_path::{geometry::Tolerance, paths::segment};
use oxvg_path::{algorithm::bool_ops::OverlayRule, geometry::Tolerance, paths::segment};

use crate::{Action, Actor, Error};
use crate::{Action, Actor, Error, state::StateElement, utils::create_oxvg_attr};

impl<'input> Actor<'input, '_> {
/// Intersects selected path definitions.
Expand All @@ -20,7 +20,54 @@ impl<'input> Actor<'input, '_> {
///
#[doc = include_str!("../../spec/manipulate/path_intersect.md")]
pub fn path_intersect(&mut self) -> Result<(), Error<'input>> {
self.state.record(&Action::PathIntersect, &self.allocator);
self.boolean_op(&Action::PathIntersect, OverlayRule::Intersect)
}

/// Unites selected path definitions.
///
/// # Errors
///
/// When root element is missing.
///
/// # Spec
///
#[doc = include_str!("../../spec/manipulate/path_union.md")]
pub fn path_union(&mut self) -> Result<(), Error<'input>> {
self.boolean_op(&Action::PathUnion, OverlayRule::Union)
}

/// Subtracts selected path definitions.
///
/// # Errors
///
/// When root element is missing.
///
/// # Spec
///
#[doc = include_str!("../../spec/manipulate/path_subtract.md")]
pub fn path_subtract(&mut self) -> Result<(), Error<'input>> {
self.boolean_op(&Action::PathSubtract, OverlayRule::Difference)
}

/// XORs selected path definitions.
///
/// # Errors
///
/// When root element is missing.
///
/// # Spec
///
#[doc = include_str!("../../spec/manipulate/path_xor.md")]
pub fn path_xor(&mut self) -> Result<(), Error<'input>> {
self.boolean_op(&Action::PathXor, OverlayRule::Xor)
}

fn boolean_op(
&mut self,
action: &Action<'input>,
overlay_rule: OverlayRule,
) -> Result<(), Error<'input>> {
self.state.record(action, &self.allocator);
let Some(selections) = self.get_selections()? else {
return Ok(());
};
Expand All @@ -30,27 +77,19 @@ impl<'input> Actor<'input, '_> {
let mut cumulative_path: Option<oxvg_path::paths::bool::Path> = None;
let mut previous_element: Option<Element> = None;
let styles: Vec<_> = style::root(&root).collect();
for selection in selections {
#[allow(clippy::cast_sign_loss)]
let Some(node) = self.allocator.get(selection as usize) else {
continue;
};
let Some(element) = node.element() else {
continue;
};
if !is_element!(element, Path) {
continue;
}
if element.has_child_nodes() {
continue;
}
let Some(path) = get_attribute!(element, D) else {
continue;
};
let segment_path = segment::Path::from_svg(&path, &Tolerance::default());
drop(path);
#[allow(clippy::cast_sign_loss)]
let paths: Vec<_> = selections
.iter()
.filter_map(|s| self.allocator.get(*s as usize))
.filter_map(oxvg_ast::node::Node::element)
.filter(|e| !e.has_child_nodes() && is_element!(e, Path) && has_attribute!(e, D))
.collect();
for path in paths {
let d = get_attribute!(path, D).unwrap();
let segment_path = segment::Path::from_svg(&d, &Tolerance::default());
drop(d);
let computed_styles = ComputedStyles::default()
.with_all(&element, &styles)
.with_all(&path, &styles)
.map_err(|err| Error::ComputedStylesError(err.to_string()))?;
let evenodd = get_computed_style!(computed_styles, FillRule)
.map(|fill_rule| match fill_rule {
Expand All @@ -65,16 +104,16 @@ impl<'input> Actor<'input, '_> {

cumulative_path = Some(match cumulative_path {
Some(inner) => oxvg_path::paths::bool::Path {
inner: inner.intersection(&segment_path),
inner: inner.boolean_op(&segment_path, overlay_rule),
evenodd: true,
},
None => segment_path,
});

if let Some(element) = previous_element {
element.remove();
if let Some(previous_element) = previous_element {
previous_element.remove();
}
previous_element = Some(element);
previous_element = Some(path);
}

if let (Some(final_element), Some(cumulative_path)) = (previous_element, cumulative_path) {
Expand All @@ -91,6 +130,18 @@ impl<'input> Actor<'input, '_> {
);
}

if let Some(selection) = selections.last() {
self.state
.get_selections(&self.allocator)
.set_attribute(create_oxvg_attr(
StateElement::SELECTION_IDS,
#[allow(clippy::cast_sign_loss)]
((*selection as usize) - selections.len())
.to_string()
.into(),
));
}
self.state.embed(self.root)?;
Ok(())
}
}
2 changes: 1 addition & 1 deletion crates/oxvg_actions/src/spec/manipulate/path_intersect.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
For selected elements with path data, merges the paths by some operation. Each merge will be applied to the selections sequentially. Merges will be effective on the front-most element.
For selected elements with path data, merges the paths by intersecting, producing a path that fills the area that the paths overlapped. Each merge will be applied to the selections sequentially. Merges will be effective on the front-most element.

A selected element will be filtered if the element is not a `path` element, has no `d` attribute, or has children. The merge will keep only the front-most element's attributes, omitting any attributes applied to other elements.

Expand Down
8 changes: 8 additions & 0 deletions crates/oxvg_actions/src/spec/manipulate/path_subtract.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
For selected elements with path data, merges the paths by subtracting, producing a path that fills the area that both the paths covered. Each merge will be applied to the selections sequentially. Merges will be effective on the front-most element.

A selected element will be filtered if the element is not a `path` element, has no `d` attribute, or has children. The merge will keep only the front-most element's attributes, omitting any attributes applied to other elements.

```sh
# Effects: History, Document, Selection
-path-unite
```
8 changes: 8 additions & 0 deletions crates/oxvg_actions/src/spec/manipulate/path_union.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
For selected elements with path data, merges the paths by uniting, producing a path that fills the area that both the paths covered. Each merge will be applied to the selections sequentially. Merges will be effective on the front-most element.

A selected element will be filtered if the element is not a `path` element, has no `d` attribute, or has children. The merge will keep only the front-most element's attributes, omitting any attributes applied to other elements.

```sh
# Effects: History, Document, Selection
-path-unite
```
8 changes: 8 additions & 0 deletions crates/oxvg_actions/src/spec/manipulate/path_xor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
For selected elements with path data, merges the paths by XOR-ing, producing a path that fills the area that neither the paths covered. Each merge will be applied to the selections sequentially. Merges will be effective on the front-most element.

A selected element will be filtered if the element is not a `path` element, has no `d` attribute, or has children. The merge will keep only the front-most element's attributes, omitting any attributes applied to other elements.

```sh
# Effects: History, Document, Selection
-path-unite
```
22 changes: 21 additions & 1 deletion crates/oxvg_actions/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,9 @@ impl<'input> Action<'input> {
const ATTR: &'static str = "Attr";
const CLASS: &'static str = "Class";
const PATH_INTERSECT: &'static str = "PathIntersect";
const PATH_UNION: &'static str = "PathUnion";
const PATH_SUBTRACT: &'static str = "PathSubtract";
const PATH_XOR: &'static str = "PathXor";
const STYLE: &'static str = "Style";
const MATRIX: &'static str = "Matrix";
const TRANSLATE: &'static str = "Translate";
Expand Down Expand Up @@ -293,6 +296,9 @@ impl<'input> Action<'input> {
Ok(Self::Class(class))
}
Self::PATH_INTERSECT => Ok(Self::PathIntersect),
Self::PATH_UNION => Ok(Self::PathUnion),
Self::PATH_SUBTRACT => Ok(Self::PathSubtract),
Self::PATH_XOR => Ok(Self::PathXor),
Self::STYLE => {
let Some(property) = args.next().transpose()? else {
return Err(Error::MissingStateAttribute(Self::ARG));
Expand Down Expand Up @@ -434,7 +440,12 @@ impl<'input> Action<'input> {
Self::Class(arg) | Self::Select(arg) | Self::SelectMore(arg) => {
Self::embed_arg(&element, allocator, arg.clone());
}
Self::PathIntersect | Self::Forget | Self::Deselect => {}
Self::PathIntersect
| Self::PathUnion
| Self::PathSubtract
| Self::PathXor
| Self::Forget
| Self::Deselect => {}
}
}

Expand All @@ -454,6 +465,9 @@ impl<'input> Action<'input> {
Self::Attr { .. } => Self::ATTR,
Self::Class(_) => Self::CLASS,
Self::PathIntersect => Self::PATH_INTERSECT,
Self::PathUnion => Self::PATH_UNION,
Self::PathSubtract => Self::PATH_SUBTRACT,
Self::PathXor => Self::PATH_XOR,
Self::Style { .. } => Self::STYLE,
Self::Matrix(..) => Self::MATRIX,
Self::Translate(..) => Self::TRANSLATE,
Expand All @@ -479,6 +493,9 @@ impl<'input> Action<'input> {
},
Self::Class(name) => ActionNapi::Class(name.to_string()),
Self::PathIntersect => ActionNapi::PathIntersect,
Self::PathUnion => ActionNapi::PathUnion,
Self::PathSubtract => ActionNapi::PathSubtract,
Self::PathXor => ActionNapi::PathXor,
Self::Style { property, value } => ActionNapi::Style {
property: property.to_string(),
value: value.to_string(),
Expand Down Expand Up @@ -511,6 +528,9 @@ impl<'input> Action<'input> {
},
ActionNapi::Class(name) => Action::Class(name.into()),
ActionNapi::PathIntersect => Action::PathIntersect,
ActionNapi::PathUnion => Action::PathUnion,
ActionNapi::PathSubtract => Action::PathSubtract,
ActionNapi::PathXor => Action::PathXor,
ActionNapi::Style { property, value } => Action::Style {
property: property.into(),
value: value.into(),
Expand Down
Loading
Loading