mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-28 19:57:05 -06:00
G13: Intertwine tool, Object › Intertwine › Edit, menu and translations (#1763)
Make and Edit choose the Intertwine tool: hovering highlights the overlap area under the pointer, a click swaps which object is on top there, and a path drawn around overlaps (Shift: a rectangle) arranges every overlap inside it; Esc or Enter goes back to the tool chosen before. The overlap faces move to vectorcraft-tools, shared by the tool and the command. Clicks inside a raised area pick the member drawn on top there, the Object menu gets the Intertwine submenu, and the new labels and the predicate message are translated in the complete catalogs. docs/gaps.md records what landed under G13.
This commit is contained in:
Generated
+1
@@ -4930,6 +4930,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"vectorcraft-color",
|
||||
"vectorcraft-doc",
|
||||
"vectorcraft-effects",
|
||||
"vectorcraft-geom",
|
||||
"vectorcraft-pathops",
|
||||
"vectorcraft-text",
|
||||
|
||||
+13
-14
@@ -292,20 +292,19 @@ fn hit_children(
|
||||
|
||||
/// The order a click at `p` tries the children of `parent` in, front first: their stacking order
|
||||
/// from the top, except that in an Intertwine group the members drawn over the others at `p`
|
||||
/// come first.
|
||||
fn hit_order(parent: &Node, children: &[std::sync::Arc<Node>], p: Point) -> Vec<usize> {
|
||||
let mut order: Vec<usize> = (0..children.len()).rev().collect();
|
||||
if matches!(parent.kind, NodeKind::Intertwine { .. }) {
|
||||
let over = |i: &usize| {
|
||||
children
|
||||
.get(*i)
|
||||
.and_then(|c| crate::intertwine::over_area(c))
|
||||
.is_some_and(|a| fill_contains(&a.to_bezpath(), vectorcraft_geom::FillRule::NonZero, p))
|
||||
};
|
||||
// A stable sort: the members drawn over the others keep their own order among them.
|
||||
order.sort_by_key(|i| !over(i));
|
||||
}
|
||||
order
|
||||
/// come first (from the top among them too).
|
||||
fn hit_order<'a>(parent: &Node, children: &'a [std::sync::Arc<Node>], p: Point) -> impl Iterator<Item = usize> + 'a {
|
||||
let over: Vec<usize> = if matches!(parent.kind, NodeKind::Intertwine { .. }) {
|
||||
let contains = |a: &vectorcraft_geom::PathData| fill_contains(&a.to_bezpath(), vectorcraft_geom::FillRule::NonZero, p);
|
||||
(0..children.len()).rev().filter(|i| children.get(*i).and_then(|c| crate::intertwine::over_area(c)).is_some_and(contains)).collect()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
let rest = (0..children.len()).rev().filter({
|
||||
let over = over.clone();
|
||||
move |i| !over.contains(i)
|
||||
});
|
||||
over.into_iter().chain(rest)
|
||||
}
|
||||
|
||||
/// A click on the strokes of a path or compound path `n` along `bp`: what any visible stroke
|
||||
|
||||
@@ -18,11 +18,10 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::intertwine::over_area;
|
||||
use vectorcraft_doc::{AppearanceItem, Document, Node, NodeId, NodeKind};
|
||||
use vectorcraft_geom::{FillRule, PathData, Point, SubPath, shapes};
|
||||
use vectorcraft_doc::{Document, Node, NodeId, NodeKind};
|
||||
use vectorcraft_geom::{FillRule, PathData, Point, SubPath};
|
||||
use vectorcraft_pathops as po;
|
||||
use vectorcraft_render::effects;
|
||||
use vectorcraft_tools::intertwine::{Face, drawn_order, face_at, faces, faces_in, is_intertwine, nonempty};
|
||||
|
||||
use super::edit::selected_roots;
|
||||
use super::pathops::Beside;
|
||||
@@ -35,10 +34,19 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Make",
|
||||
["Object", "Intertwine"],
|
||||
None,
|
||||
"{ids?: two or more objects (default: the selection)} put the objects in an Intertwine group, where overlap areas can be arranged with object.intertwine.arrange → {id}",
|
||||
"{ids?: two or more objects (default: the selection)} put the objects in an Intertwine group and choose the Intertwine tool, which arranges its overlap areas (object.intertwine.arrange) → {id}",
|
||||
has_selection,
|
||||
make
|
||||
),
|
||||
cmd!(
|
||||
"object.intertwine.edit",
|
||||
"Edit",
|
||||
["Object", "Intertwine"],
|
||||
None,
|
||||
"{} choose the Intertwine tool for the selected Intertwine group (Esc or Enter goes back to the tool before)",
|
||||
has_intertwine,
|
||||
edit
|
||||
),
|
||||
cmd!(
|
||||
"object.intertwine.release",
|
||||
"Release",
|
||||
@@ -53,7 +61,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Arrange Intertwine Area",
|
||||
[],
|
||||
None,
|
||||
"{id?: Intertwine group (default: the selected one), at?: [x,y] (the overlap area under this point, as a click picks it) | area?: [[x,y]…] (a closed polygon drawn around overlap areas: every area whose inside it encloses), order?: toggle|front|back (toggle: the object drawn right below the top comes to the front, so two objects swap), member?: id (front and back: the object to move; default: back moves the top one)} → {count: areas changed, top: [id of the object now on top in each area]}",
|
||||
"{id?: Intertwine group (default: the selected one), at?: [x,y] (the overlap area under this point, as a click picks it) | area?: [[x,y]…] (a closed polygon drawn around overlap areas: every area whose inside it encloses), order?: toggle|front|back (toggle: the object drawn right below the top comes to the front, so two objects swap), member?: id (front and back: the object to move; default: front brings the object right below the top, back sends the top one)} → {count: areas changed, top: [id of the object now on top in each area]}",
|
||||
has_intertwine,
|
||||
arrange
|
||||
),
|
||||
@@ -77,10 +85,6 @@ fn selected_groups(s: &Session) -> Vec<NodeId> {
|
||||
out
|
||||
}
|
||||
|
||||
fn is_intertwine(n: &Node) -> bool {
|
||||
matches!(n.kind, NodeKind::Intertwine { .. })
|
||||
}
|
||||
|
||||
fn has_intertwine(s: &Session) -> std::result::Result<(), String> {
|
||||
has_selection(s)?;
|
||||
if selected_groups(s).is_empty() { Err("select an Intertwine group".into()) } else { Ok(()) }
|
||||
@@ -91,7 +95,8 @@ fn has_intertwine(s: &Session) -> std::result::Result<(), String> {
|
||||
/// Why `n` can't go into an Intertwine group, if it can't (the help page's limits: no nested
|
||||
/// Intertwine groups, Live Paint, Repeat or graphs).
|
||||
fn refused(d: &Document, n: &Node) -> Option<&'static str> {
|
||||
if d.ancestry(n.id).unwrap_or_default().iter().filter_map(|a| d.node(*a)).any(is_intertwine) {
|
||||
let around = d.ancestry(n.id).unwrap_or_default();
|
||||
if around.iter().filter(|a| **a != n.id).filter_map(|a| d.node(*a)).any(is_intertwine) {
|
||||
return Some("an object already in an Intertwine group");
|
||||
}
|
||||
let mut why = None;
|
||||
@@ -134,9 +139,26 @@ fn make(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
sel.set([id]);
|
||||
Ok(id)
|
||||
})?;
|
||||
choose_tool(s)?;
|
||||
Ok(json!({ "id": id.0 }))
|
||||
}
|
||||
|
||||
/// Choose the Intertwine tool, which goes back to the tool chosen now.
|
||||
fn choose_tool(s: &mut Session) -> Result<()> {
|
||||
let prev = Some(s.tool_id()).filter(|t| *t != "intertwine");
|
||||
let view = s.last_view;
|
||||
s.select_tool("intertwine", view)?;
|
||||
s.set_tool_option("prev", &json!(prev));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn edit(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
let [g] = selected_groups(s)[..] else { return Err(bad("object.intertwine.edit", "select one Intertwine group")) };
|
||||
s.select(|_, sel| sel.set([g]))?;
|
||||
choose_tool(s)?;
|
||||
Ok(json!({ "id": g.0 }))
|
||||
}
|
||||
|
||||
fn release(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
let groups = selected_groups(s);
|
||||
let ids = s.edit("Release Intertwine", |d, sel| {
|
||||
@@ -173,118 +195,19 @@ enum Order {
|
||||
Back,
|
||||
}
|
||||
|
||||
/// The area `n` paints, in document coordinates, filled non-zero: its visible fills and stroke
|
||||
/// outlines; type by its outlines; groups by their visible members; other objects by their
|
||||
/// bounds. Effects are left out. `None` when it paints nothing.
|
||||
pub(crate) fn painted_area(n: &Node) -> Option<PathData> {
|
||||
if !n.visible {
|
||||
return None;
|
||||
}
|
||||
let mut parts: Vec<PathData> = vec![];
|
||||
let shape = match &n.kind {
|
||||
NodeKind::Path { path, rule, guide: false, clipping: false, .. } => Some((path.clone(), *rule)),
|
||||
NodeKind::Compound { children, rule } => {
|
||||
let subs: Vec<SubPath> = children.iter().filter_map(|c| c.path_data()).flat_map(|p| p.subpaths.iter().cloned()).collect();
|
||||
Some((PathData::new(subs), *rule))
|
||||
}
|
||||
NodeKind::Path { .. } => return None,
|
||||
NodeKind::Group { children, clip: true } => {
|
||||
// What the members paint inside the clipping path.
|
||||
let clip = children.first().and_then(|c| match &c.kind {
|
||||
NodeKind::Path { path, rule, .. } => Some(po::normalize(path, *rule)),
|
||||
_ => c.geometric_bounds().map(shapes::rectangle),
|
||||
})?;
|
||||
let inside = unite(children.iter().skip(1).filter_map(|c| painted_area(c)).collect())?;
|
||||
return nonempty(po::boolean(&inside, FillRule::NonZero, &clip, FillRule::NonZero, po::BoolOp::Intersect));
|
||||
}
|
||||
NodeKind::Group { children, .. } | NodeKind::Layer { children, .. } | NodeKind::Intertwine { children } => {
|
||||
return unite(children.iter().filter_map(|c| painted_area(c)).collect());
|
||||
}
|
||||
NodeKind::Text(_) => return effects::outline_text(n).as_ref().and_then(painted_area),
|
||||
_ => None,
|
||||
};
|
||||
match shape {
|
||||
Some((path, rule)) => {
|
||||
let filled = n.appearance.items.iter().any(|i| matches!(i, AppearanceItem::Fill(f) if f.visible && !f.paint.is_none()));
|
||||
if filled {
|
||||
parts.push(po::normalize(&path, rule));
|
||||
}
|
||||
for st in n.appearance.painted_strokes().filter(|st| st.width > 0.0 && st.width.is_finite()) {
|
||||
parts.push(effects::stroke::outline_region(&path, rule, st));
|
||||
}
|
||||
}
|
||||
None => parts.push(shapes::rectangle(n.visual_bounds()?)),
|
||||
}
|
||||
unite(parts)
|
||||
}
|
||||
/// The most points a drawn `area` may have (a long freehand loop has a few thousand).
|
||||
const MAX_AREA_POINTS: usize = 100_000;
|
||||
|
||||
fn unite(parts: Vec<PathData>) -> Option<PathData> {
|
||||
let parts: Vec<PathData> = parts.into_iter().filter(|p| !p.is_empty()).collect();
|
||||
match parts.len() {
|
||||
0 => None,
|
||||
1 => parts.into_iter().next(),
|
||||
_ => nonempty(po::unite_all(&parts.iter().map(|p| (p, FillRule::NonZero)).collect::<Vec<_>>())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Areas smaller than this (pt²) are what a boolean leaves of an area taken from itself.
|
||||
const SLIVER: f64 = 1e-3;
|
||||
|
||||
fn nonempty(p: PathData) -> Option<PathData> {
|
||||
(!p.is_empty() && po::area(&p, FillRule::NonZero).abs() > SLIVER).then_some(p)
|
||||
}
|
||||
|
||||
/// A closed polygon from `[[x,y]…]` (three points or more, finite).
|
||||
/// A closed polygon from `[[x,y]…]` (three to [`MAX_AREA_POINTS`] points, finite).
|
||||
fn polygon(v: &Value) -> Option<PathData> {
|
||||
let pts: Vec<Point> = v.as_array()?.iter().map(|q| Some(Point::new(q.get(0)?.as_f64()?, q.get(1)?.as_f64()?))).collect::<Option<_>>()?;
|
||||
let pts = v.as_array().filter(|a| a.len() <= MAX_AREA_POINTS)?;
|
||||
let pts: Vec<Point> = pts.iter().map(|q| Some(Point::new(q.get(0)?.as_f64()?, q.get(1)?.as_f64()?))).collect::<Option<_>>()?;
|
||||
if pts.len() < 3 || pts.iter().any(|q| !(q.x.is_finite() && q.y.is_finite())) {
|
||||
return None;
|
||||
}
|
||||
Some(PathData::single(SubPath::polyline(&pts, true)))
|
||||
}
|
||||
|
||||
fn contains(area: &PathData, p: Point) -> bool {
|
||||
vectorcraft_geom::hit::fill_contains(&area.to_bezpath(), FillRule::NonZero, p)
|
||||
}
|
||||
|
||||
/// One overlap area to arrange: its outline, a point inside it, and the members (child indices,
|
||||
/// bottom first) that paint there.
|
||||
struct Face {
|
||||
path: PathData,
|
||||
inside: Point,
|
||||
members: Vec<usize>,
|
||||
}
|
||||
|
||||
/// The overlap areas of `children` that `at` lies in, or that `lasso` encloses.
|
||||
fn faces(children: &[Arc<Node>], at: Option<Point>, lasso: Option<&PathData>) -> Vec<Face> {
|
||||
let shapes: Vec<po::Shape> =
|
||||
children.iter().enumerate().filter_map(|(i, c)| Some(po::Shape::new(painted_area(c)?, FillRule::NonZero, i as u64))).collect();
|
||||
let key = |k: usize| shapes.get(k).map(|s| s.key as usize);
|
||||
po::regions(&shapes)
|
||||
.into_iter()
|
||||
.filter(|r| r.sources.len() >= 2)
|
||||
.filter_map(|r| {
|
||||
let inside = match at {
|
||||
Some(p) => r.contains(p).then_some(p)?,
|
||||
None => po::interior_point(&r.path)?,
|
||||
};
|
||||
if lasso.is_some_and(|l| !contains(l, inside)) {
|
||||
return None;
|
||||
}
|
||||
Some(Face { members: r.sources.iter().filter_map(|k| key(*k)).collect(), path: r.path, inside })
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The members of `face` in the order they are drawn there, bottom first: those drawn in their
|
||||
/// own order, then those drawn over the others there, each run in the group's order.
|
||||
fn drawn_order(children: &[Arc<Node>], face: &Face) -> Vec<usize> {
|
||||
let over = |i: &usize| children.get(*i).and_then(|c| over_area(c)).is_some_and(|a| contains(a, face.inside));
|
||||
let mut order = face.members.clone();
|
||||
order.sort_by_key(|i| (over(i), *i));
|
||||
order
|
||||
}
|
||||
|
||||
/// `n`'s area with `face` added (`add`) or taken out.
|
||||
fn with_area(n: &mut Node, face: &PathData, add: bool) {
|
||||
let old = n.extras().intertwine.as_deref().cloned();
|
||||
@@ -341,7 +264,7 @@ fn arrange(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
None => None,
|
||||
};
|
||||
let lasso = match p.get("area") {
|
||||
Some(v) => Some(polygon(v).ok_or_else(|| bad(C, "area must be a closed polygon of three or more [x, y] points"))?),
|
||||
Some(v) => Some(polygon(v).ok_or_else(|| bad(C, "area must be a closed polygon of 3 to 100000 [x, y] points"))?),
|
||||
None => None,
|
||||
};
|
||||
if at.is_some() == lasso.is_some() {
|
||||
@@ -365,7 +288,12 @@ fn arrange(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Some(m) => Some(children.iter().position(|c| c.id == m).ok_or_else(|| bad(C, format!("object {} is not in this Intertwine group", m.0)))?),
|
||||
None => None,
|
||||
};
|
||||
let found = faces(&children, at, lasso.as_ref());
|
||||
let all = faces(&children);
|
||||
let found = match (at, &lasso) {
|
||||
(Some(p), _) => face_at(&all, p).into_iter().collect(),
|
||||
(None, Some(l)) => faces_in(&all, l),
|
||||
(None, None) => vec![],
|
||||
};
|
||||
if found.is_empty() {
|
||||
return Err(bad(
|
||||
C,
|
||||
@@ -389,7 +317,7 @@ fn arrange(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let drawn = drawn_order(children, face);
|
||||
let (x, front) = match (order, member) {
|
||||
(Order::Toggle, _) => (drawn.iter().rev().nth(1).copied(), true),
|
||||
(Order::Front, m) => (m.or(drawn.last().copied()), true),
|
||||
(Order::Front, m) => (m.or(drawn.iter().rev().nth(1).copied()), true),
|
||||
(Order::Back, m) => (m.or(drawn.last().copied()), false),
|
||||
};
|
||||
let Some(x) = x else { continue };
|
||||
|
||||
@@ -59,7 +59,7 @@ fn make_puts_the_objects_in_an_intertwine_group_drawn_as_before() {
|
||||
assert!(matches!(n.kind, NodeKind::Intertwine { .. }));
|
||||
assert_eq!(n.kind_label(), "Intertwine");
|
||||
assert_eq!(n.children().unwrap().iter().map(|c| c.id).collect::<Vec<_>>(), [a, b], "the stacking order is kept");
|
||||
assert_eq!(s.doc().unwrap().selection.objects.iter().copied().collect::<Vec<_>>(), [g]);
|
||||
assert_eq!(s.doc().unwrap().selection.objects.to_vec(), [g]);
|
||||
let r = render(&s);
|
||||
assert!(blue(&r, 50, 30) && blue(&r, 50, 70), "B is in front at both crossings until an area is arranged");
|
||||
}
|
||||
@@ -187,7 +187,7 @@ fn what_cannot_be_intertwined_is_refused_with_a_reason() {
|
||||
let g = id_of(&s.execute("object.intertwine.make", &json!({})).unwrap());
|
||||
let c = id_of(&s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 5, "height": 5})).unwrap());
|
||||
let e = s.execute("object.intertwine.make", &json!({"ids": [g.0, c.0]})).unwrap_err();
|
||||
assert!(e.to_string().contains("Intertwine group can't"), "{e}");
|
||||
assert!(e.to_string().contains("an Intertwine group can't") && !e.to_string().contains("already"), "{e}");
|
||||
let e = s.execute("object.intertwine.make", &json!({"ids": [a.0, c.0]})).unwrap_err();
|
||||
assert!(e.to_string().contains("already in an Intertwine group"), "{e}");
|
||||
select(&mut s, &[g]);
|
||||
@@ -202,3 +202,69 @@ fn what_cannot_be_intertwined_is_refused_with_a_reason() {
|
||||
let e = s.execute("object.intertwine.arrange", &json!({"id": c.0, "at": [1, 1]})).unwrap_err();
|
||||
assert!(e.to_string().contains("not an Intertwine group"), "{e}");
|
||||
}
|
||||
|
||||
// ---------- on the canvas ----------
|
||||
|
||||
fn press(s: &mut Session, pts: &[(f64, f64)], shift: bool) -> Vec<UiRequest> {
|
||||
use vectorcraft_tools::{Mods, PointerEvent, PointerKind};
|
||||
let view = ViewInfo::default();
|
||||
let ev = |kind, (x, y): (f64, f64)| {
|
||||
let mut e = PointerEvent::new(kind, x, y);
|
||||
e.mods = Mods { shift, ..Mods::default() };
|
||||
e
|
||||
};
|
||||
let (first, last) = (pts[0], pts[pts.len() - 1]);
|
||||
s.pointer(&ev(PointerKind::Move, first), view).unwrap();
|
||||
s.pointer(&ev(PointerKind::Down, first), view).unwrap();
|
||||
for p in &pts[1..] {
|
||||
s.pointer(&ev(PointerKind::Drag, *p), view).unwrap();
|
||||
}
|
||||
s.pointer(&ev(PointerKind::Up, last), view).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn make_chooses_the_intertwine_tool_and_a_click_on_an_overlap_arranges_it() {
|
||||
let mut s = session();
|
||||
let view = ViewInfo::default();
|
||||
s.select_tool("ellipse", view).unwrap();
|
||||
intertwined(&mut s);
|
||||
assert_eq!(s.tool_id(), "intertwine");
|
||||
// Hovering the overlap highlights it.
|
||||
s.pointer(&vectorcraft_tools::PointerEvent::new(vectorcraft_tools::PointerKind::Move, 50.0, 30.0), view).unwrap();
|
||||
assert_eq!(s.overlays(view).len(), 1);
|
||||
press(&mut s, &[(50.0, 30.0)], false);
|
||||
let r = render(&s);
|
||||
assert!(red(&r, 50, 30) && blue(&r, 50, 70));
|
||||
// A click off any overlap changes nothing (and reports no error).
|
||||
press(&mut s, &[(5.0, 5.0)], false);
|
||||
assert!(red(&render(&s), 50, 30));
|
||||
// Esc goes back to the tool chosen before Make.
|
||||
let back = s.tool_key(vectorcraft_tools::ToolKey::Escape, Default::default(), view).unwrap();
|
||||
assert_eq!(back, [UiRequest::SwitchTool("ellipse".into())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_drag_around_overlaps_arranges_them_and_shift_drags_a_rectangle() {
|
||||
let mut s = session();
|
||||
intertwined(&mut s);
|
||||
// A loop around the bottom crossing.
|
||||
press(&mut s, &[(40.0, 60.0), (60.0, 60.0), (60.0, 80.0), (40.0, 80.0), (40.0, 61.0)], false);
|
||||
let r = render(&s);
|
||||
assert!(blue(&r, 50, 30) && red(&r, 50, 70));
|
||||
// A rectangle around both.
|
||||
press(&mut s, &[(40.0, 20.0), (50.0, 50.0), (60.0, 80.0)], true);
|
||||
let r = render(&s);
|
||||
assert!(red(&r, 50, 30) && blue(&r, 50, 70));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_chooses_the_tool_for_the_group_of_a_selected_member() {
|
||||
let mut s = session();
|
||||
let (g, a, _) = intertwined(&mut s);
|
||||
s.select_tool("selection", ViewInfo::default()).unwrap();
|
||||
select(&mut s, &[a]);
|
||||
let v = s.execute("object.intertwine.edit", &json!({})).unwrap();
|
||||
assert_eq!(v["id"], g.0);
|
||||
assert_eq!(s.tool_id(), "intertwine");
|
||||
assert_eq!(s.doc().unwrap().selection.objects.to_vec(), [g]);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ vectorcraft-geom = { workspace = true }
|
||||
vectorcraft-color = { workspace = true }
|
||||
vectorcraft-doc = { workspace = true }
|
||||
vectorcraft-pathops = { workspace = true }
|
||||
vectorcraft-effects = { workspace = true }
|
||||
vectorcraft-text = { workspace = true }
|
||||
kurbo = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
|
||||
@@ -0,0 +1,343 @@
|
||||
//! Object › Intertwine (#1763): the overlap areas of an Intertwine group, and the tool that
|
||||
//! arranges them on the canvas.
|
||||
//!
|
||||
//! An overlap area is a face of the planar arrangement of what the group's members paint (their
|
||||
//! fills and strokes, type outlined; other objects by their bounds): a part where two or more of
|
||||
//! them overlap, split wherever a member's edge crosses it. `object.intertwine.arrange` changes
|
||||
//! which member is drawn on top there.
|
||||
//!
|
||||
//! The Intertwine tool is what `object.intertwine.make` and `object.intertwine.edit` leave the
|
||||
//! canvas in: hovering highlights the overlap area under the pointer, a click arranges it, a drag
|
||||
//! draws a path around overlap areas (Shift: a rectangle) and arranges every one inside it.
|
||||
//! Esc or Enter goes back to the tool chosen before. Behaviour:
|
||||
//! <https://helpx.adobe.com/illustrator/using/intertwine-objects.html>.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use kurbo::BezPath;
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::intertwine::over_area;
|
||||
use vectorcraft_doc::{AppearanceItem, Document, Node, NodeId, NodeKind};
|
||||
use vectorcraft_effects as effects;
|
||||
use vectorcraft_geom::{FillRule, PathData, Point, Rect, SubPath, shapes};
|
||||
use vectorcraft_pathops as po;
|
||||
|
||||
use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey};
|
||||
|
||||
/// The area `n` paints, in document coordinates, filled non-zero: its visible fills and stroke
|
||||
/// outlines; type by its outlines; groups by their visible members (clip groups inside their
|
||||
/// clipping path); other objects by their bounds. Effects are left out. `None` when it paints
|
||||
/// nothing.
|
||||
pub fn painted_area(n: &Node) -> Option<PathData> {
|
||||
if !n.visible {
|
||||
return None;
|
||||
}
|
||||
let shape = match &n.kind {
|
||||
NodeKind::Path { guide: true, .. } | NodeKind::Path { clipping: true, .. } => return None,
|
||||
NodeKind::Path { path, rule, .. } => Some((path.clone(), *rule)),
|
||||
NodeKind::Compound { children, rule } => {
|
||||
let subs: Vec<SubPath> = children.iter().filter_map(|c| c.path_data()).flat_map(|p| p.subpaths.iter().cloned()).collect();
|
||||
Some((PathData::new(subs), *rule))
|
||||
}
|
||||
NodeKind::Group { children, clip: true } => {
|
||||
let clip = children.first().and_then(|c| match &c.kind {
|
||||
NodeKind::Path { path, rule, .. } => Some(po::normalize(path, *rule)),
|
||||
_ => c.geometric_bounds().map(shapes::rectangle),
|
||||
})?;
|
||||
let inside = unite(children.iter().skip(1).filter_map(|c| painted_area(c)).collect())?;
|
||||
return nonempty(po::boolean(&inside, FillRule::NonZero, &clip, FillRule::NonZero, po::BoolOp::Intersect));
|
||||
}
|
||||
NodeKind::Group { children, .. } | NodeKind::Layer { children, .. } | NodeKind::Intertwine { children } => {
|
||||
return unite(children.iter().filter_map(|c| painted_area(c)).collect());
|
||||
}
|
||||
NodeKind::Text(_) => return effects::outline_text(n).as_ref().and_then(painted_area),
|
||||
_ => None,
|
||||
};
|
||||
let mut parts: Vec<PathData> = vec![];
|
||||
match shape {
|
||||
Some((path, rule)) => {
|
||||
if n.appearance.items.iter().any(|i| matches!(i, AppearanceItem::Fill(f) if f.visible && !f.paint.is_none())) {
|
||||
parts.push(po::normalize(&path, rule));
|
||||
}
|
||||
for st in n.appearance.painted_strokes().filter(|st| st.width > 0.0 && st.width.is_finite()) {
|
||||
parts.push(effects::stroke::outline_region(&path, rule, st));
|
||||
}
|
||||
}
|
||||
None => parts.push(shapes::rectangle(n.visual_bounds()?)),
|
||||
}
|
||||
unite(parts)
|
||||
}
|
||||
|
||||
fn unite(parts: Vec<PathData>) -> Option<PathData> {
|
||||
let parts: Vec<PathData> = parts.into_iter().filter(|p| !p.is_empty()).collect();
|
||||
match parts.len() {
|
||||
0 => None,
|
||||
1 => parts.into_iter().next(),
|
||||
_ => nonempty(po::unite_all(&parts.iter().map(|p| (p, FillRule::NonZero)).collect::<Vec<_>>())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Areas smaller than this (pt²) are what a boolean leaves of an area taken from itself.
|
||||
pub const SLIVER: f64 = 1e-3;
|
||||
|
||||
/// `p`, unless it is empty or a sliver.
|
||||
pub fn nonempty(p: PathData) -> Option<PathData> {
|
||||
(!p.is_empty() && po::area(&p, FillRule::NonZero).abs() > SLIVER).then_some(p)
|
||||
}
|
||||
|
||||
/// Does area `a` (filled non-zero) contain `p`?
|
||||
pub fn contains(a: &PathData, p: Point) -> bool {
|
||||
vectorcraft_geom::hit::fill_contains(&a.to_bezpath(), FillRule::NonZero, p)
|
||||
}
|
||||
|
||||
/// One overlap area: its outline, a point inside it, and the members (child indices, bottom
|
||||
/// first) that paint there.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Face {
|
||||
pub path: PathData,
|
||||
pub inside: Point,
|
||||
pub members: Vec<usize>,
|
||||
}
|
||||
|
||||
/// Every overlap area of `children`.
|
||||
pub fn faces(children: &[Arc<Node>]) -> Vec<Face> {
|
||||
let shapes: Vec<po::Shape> =
|
||||
children.iter().enumerate().filter_map(|(i, c)| Some(po::Shape::new(painted_area(c)?, FillRule::NonZero, i as u64))).collect();
|
||||
let key = |k: usize| shapes.get(k).map(|s| s.key as usize);
|
||||
po::regions(&shapes)
|
||||
.into_iter()
|
||||
.filter(|r| r.sources.len() >= 2)
|
||||
.filter_map(|r| {
|
||||
let inside = po::interior_point(&r.path)?;
|
||||
Some(Face { members: r.sources.iter().filter_map(|k| key(*k)).collect(), path: r.path, inside })
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The overlap area of `faces` under `p`, with `p` as its inside point (a click picks it).
|
||||
pub fn face_at(faces: &[Face], p: Point) -> Option<Face> {
|
||||
faces.iter().find(|f| contains(&f.path, p)).map(|f| Face { inside: p, ..f.clone() })
|
||||
}
|
||||
|
||||
/// The overlap areas of `faces` whose inside `lasso` (a closed path) encloses.
|
||||
pub fn faces_in(faces: &[Face], lasso: &PathData) -> Vec<Face> {
|
||||
faces.iter().filter(|f| contains(lasso, f.inside)).cloned().collect()
|
||||
}
|
||||
|
||||
/// The members of `face` in the order they are drawn there, bottom first: those drawn in their
|
||||
/// own order, then those drawn over the others there, each run in the group's order.
|
||||
pub fn drawn_order(children: &[Arc<Node>], face: &Face) -> Vec<usize> {
|
||||
let over = |i: &usize| children.get(*i).and_then(|c| over_area(c)).is_some_and(|a| contains(a, face.inside));
|
||||
let mut order = face.members.clone();
|
||||
order.sort_by_key(|i| (over(i), *i));
|
||||
order
|
||||
}
|
||||
|
||||
/// The Intertwine group the selection is in, or is (the innermost one around the first selected
|
||||
/// object that is in one).
|
||||
pub fn selected_group(doc: &Document, selected: &[NodeId]) -> Option<NodeId> {
|
||||
selected.iter().find_map(|id| doc.ancestry(*id)?.into_iter().rev().find(|a| doc.node(*a).is_some_and(is_intertwine)))
|
||||
}
|
||||
|
||||
pub fn is_intertwine(n: &Node) -> bool {
|
||||
matches!(n.kind, NodeKind::Intertwine { .. })
|
||||
}
|
||||
|
||||
// ---------- the tool ----------
|
||||
|
||||
/// The overlap areas of an Intertwine group, kept for the document revision they were found in.
|
||||
struct Found {
|
||||
revision: (u64, u64),
|
||||
group: NodeId,
|
||||
faces: Arc<Vec<Face>>,
|
||||
}
|
||||
|
||||
/// The Intertwine tool (not in the toolbar: Make and Edit choose it).
|
||||
#[derive(Default)]
|
||||
pub struct IntertwineTool {
|
||||
/// The tool to go back to (Esc, Enter).
|
||||
prev: Option<String>,
|
||||
/// The overlap areas of the group, for the document revision they were found in.
|
||||
cache: Option<Found>,
|
||||
/// The overlap area under the pointer.
|
||||
hover: Option<BezPath>,
|
||||
/// A drag's points (document space); the first is where it started.
|
||||
points: Vec<Point>,
|
||||
/// The drag draws a rectangle (Shift at its start).
|
||||
rect: bool,
|
||||
dragging: bool,
|
||||
}
|
||||
|
||||
impl IntertwineTool {
|
||||
fn faces(&mut self, cx: &ToolContext) -> Option<(NodeId, Arc<Vec<Face>>)> {
|
||||
let g = selected_group(cx.doc, &cx.selection.objects)?;
|
||||
if let Some(c) = &self.cache
|
||||
&& c.revision == cx.revision
|
||||
&& c.group == g
|
||||
{
|
||||
return Some((g, c.faces.clone()));
|
||||
}
|
||||
let children = cx.doc.node(g)?.children()?;
|
||||
let f = Arc::new(faces(children));
|
||||
self.cache = Some(Found { revision: cx.revision, group: g, faces: f.clone() });
|
||||
Some((g, f))
|
||||
}
|
||||
|
||||
/// The closed path the drag drew.
|
||||
fn lasso(&self) -> Option<PathData> {
|
||||
let (first, last) = (*self.points.first()?, *self.points.last()?);
|
||||
let pts = if self.rect {
|
||||
let r = Rect::from_points(first, last);
|
||||
vec![Point::new(r.x0, r.y0), Point::new(r.x1, r.y0), Point::new(r.x1, r.y1), Point::new(r.x0, r.y1)]
|
||||
} else {
|
||||
self.points.clone()
|
||||
};
|
||||
(pts.len() >= 3).then(|| PathData::single(SubPath::polyline(&pts, true)))
|
||||
}
|
||||
|
||||
fn back(&self) -> Vec<Action> {
|
||||
vec![Action::SwitchTool(self.prev.clone().unwrap_or_else(|| "selection".into()))]
|
||||
}
|
||||
}
|
||||
|
||||
impl Tool for IntertwineTool {
|
||||
fn id(&self) -> &'static str {
|
||||
"intertwine"
|
||||
}
|
||||
|
||||
fn pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec<Action> {
|
||||
let p = ev.pos;
|
||||
match ev.kind {
|
||||
PointerKind::Move => {
|
||||
self.hover = self.faces(cx).and_then(|(_, f)| face_at(&f, p)).map(|f| f.path.to_bezpath());
|
||||
vec![]
|
||||
}
|
||||
PointerKind::Down => {
|
||||
self.dragging = true;
|
||||
self.rect = ev.mods.shift;
|
||||
self.points = vec![p];
|
||||
vec![]
|
||||
}
|
||||
PointerKind::Drag if self.dragging => {
|
||||
if self.points.last().is_none_or(|q| q.distance(p) >= cx.tol(2.0)) {
|
||||
self.points.push(p);
|
||||
}
|
||||
vec![]
|
||||
}
|
||||
PointerKind::Up if self.dragging => {
|
||||
self.dragging = false;
|
||||
let start = self.points.first().copied().unwrap_or(p);
|
||||
self.points.push(p);
|
||||
let Some((g, faces)) = self.faces(cx) else { return vec![] };
|
||||
// A drag that never left the press is a click (a closed loop ends where it began).
|
||||
let click = self.points.iter().all(|q| q.distance(start) < cx.tol(3.0));
|
||||
let lasso = if click { None } else { self.lasso() };
|
||||
self.points.clear();
|
||||
let params = match lasso {
|
||||
// Only where there is something to arrange: a miss changes nothing.
|
||||
Some(l) if !faces_in(&faces, &l).is_empty() => {
|
||||
let area: Vec<Value> = l.subpaths.iter().flat_map(|s| s.anchors.iter()).map(|a| json!([a.p.x, a.p.y])).collect();
|
||||
json!({ "id": g.0, "area": area })
|
||||
}
|
||||
None if face_at(&faces, p).is_some() => json!({ "id": g.0, "at": [p.x, p.y] }),
|
||||
_ => return vec![],
|
||||
};
|
||||
self.hover = None;
|
||||
vec![Action::Exec("object.intertwine.arrange".into(), params)]
|
||||
}
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn key(&mut self, _cx: &ToolContext, key: ToolKey, _mods: Mods) -> Vec<Action> {
|
||||
match key {
|
||||
ToolKey::Escape | ToolKey::Enter if !self.dragging => self.back(),
|
||||
ToolKey::Escape => {
|
||||
self.dragging = false;
|
||||
self.points.clear();
|
||||
vec![]
|
||||
}
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn claims_key(&self, _cx: &ToolContext, key: ToolKey) -> bool {
|
||||
matches!(key, ToolKey::Escape | ToolKey::Enter)
|
||||
}
|
||||
|
||||
fn overlays(&self, _cx: &ToolContext) -> Vec<Overlay> {
|
||||
let color = crate::builder::HIGHLIGHT_RED;
|
||||
let mut out = vec![];
|
||||
if self.dragging && self.points.len() > 1 {
|
||||
if let Some(l) = self.lasso() {
|
||||
out.push(Overlay::Path { path: l.to_bezpath(), color, width: 1.0, dashed: true });
|
||||
}
|
||||
} else if let Some(h) = &self.hover {
|
||||
out.push(Overlay::Path { path: h.clone(), color, width: 2.0, dashed: false });
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn cursor(&self, _cx: &ToolContext, _p: Point, _mods: Mods) -> Cursor {
|
||||
Cursor::Arrow
|
||||
}
|
||||
|
||||
fn options(&self) -> Value {
|
||||
json!({ "prev": self.prev })
|
||||
}
|
||||
|
||||
fn set_option(&mut self, key: &str, value: &Value) {
|
||||
if key == "prev" {
|
||||
self.prev = value.as_str().filter(|t| *t != "intertwine").map(str::to_string);
|
||||
}
|
||||
}
|
||||
|
||||
fn busy(&self) -> bool {
|
||||
self.dragging
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create(id: &str) -> Option<Box<dyn Tool>> {
|
||||
(id == "intertwine").then(|| Box::new(IntertwineTool::default()) as Box<dyn Tool>)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use vectorcraft_doc::{Appearance, NodeId};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn square(id: u64, x: f64) -> Arc<Node> {
|
||||
Arc::new(Node::path(NodeId(id), shapes::rectangle(Rect::new(x, 0.0, x + 10.0, 10.0)), Appearance::default_art()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_overlapping_squares_have_one_overlap_area() {
|
||||
let kids = vec![square(1, 0.0), square(2, 5.0), square(3, 40.0)];
|
||||
let f = faces(&kids);
|
||||
assert_eq!(f.len(), 1);
|
||||
assert_eq!(f[0].members, [0, 1]);
|
||||
let at = face_at(&f, Point::new(7.0, 5.0)).unwrap();
|
||||
assert_eq!(at.inside, Point::new(7.0, 5.0));
|
||||
assert!(face_at(&f, Point::new(2.0, 5.0)).is_none(), "one square alone is no overlap");
|
||||
assert_eq!(drawn_order(&kids, &at), [0, 1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_member_with_an_area_there_is_drawn_last() {
|
||||
let mut a = (*square(1, 0.0)).clone();
|
||||
a.extras_mut().intertwine = Some(Box::new(shapes::rectangle(Rect::new(5.0, 0.0, 10.0, 10.0))));
|
||||
let kids = vec![Arc::new(a), square(2, 5.0)];
|
||||
let f = faces(&kids);
|
||||
assert_eq!(drawn_order(&kids, &f[0]), [1, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_lasso_takes_the_areas_whose_inside_it_encloses() {
|
||||
let kids = vec![square(1, 0.0), square(2, 5.0)];
|
||||
let f = faces(&kids);
|
||||
let around = |r: Rect| shapes::rectangle(r);
|
||||
assert_eq!(faces_in(&f, &around(Rect::new(4.0, -1.0, 11.0, 11.0))).len(), 1);
|
||||
assert!(faces_in(&f, &around(Rect::new(20.0, 0.0, 30.0, 10.0))).is_empty());
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ pub mod distort;
|
||||
pub mod draw2;
|
||||
pub mod extra;
|
||||
pub mod guides;
|
||||
pub mod intertwine;
|
||||
pub mod meshblend;
|
||||
pub mod meshedit;
|
||||
pub mod params;
|
||||
@@ -622,6 +623,7 @@ pub fn create(id: &str) -> Box<dyn Tool> {
|
||||
.or_else(|| printtiling::create(other))
|
||||
.or_else(|| cropimage::create(other))
|
||||
.or_else(|| cut::create(other))
|
||||
.or_else(|| intertwine::create(other))
|
||||
.unwrap_or_else(|| Box::new(NoopTool(tool_info(other).map(|t| t.id).unwrap_or("selection")))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -425,6 +425,8 @@
|
||||
Add Přidat
|
||||
@id pathfinder.add Sjednotit
|
||||
Intersect Protnout
|
||||
Intertwine Proplést
|
||||
Arrange Intertwine Area Uspořádat oblast propletení
|
||||
Exclude Vyloučit
|
||||
Subtract Odečíst
|
||||
Minus Back Odečíst zadní
|
||||
@@ -4197,6 +4199,7 @@ imageTrace Outline Obrys
|
||||
@msg select a gradient mesh vyberte přechodovou síť
|
||||
@msg select a linked image: an embedded one has no file vyberte propojený obrázek: vložený nemá soubor
|
||||
@msg select a repeat vyberte opakování
|
||||
@msg select an Intertwine group vyberte skupinu propletení
|
||||
@msg select a stroke with a variable width vyberte tah s proměnnou šířkou
|
||||
@msg select a symbol instance vyberte instanci symbolu
|
||||
@msg select an Image Trace object vyberte objekt vektorizace obrazu
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1357,6 +1357,8 @@
|
||||
Internal CSS Internes CSS
|
||||
Internet Standard RGB (sRGB) Internet-Standard-RGB (sRGB)
|
||||
Intersect Schnittmenge bilden
|
||||
Intertwine Verflechten
|
||||
Arrange Intertwine Area Überlappungsbereich verflechten
|
||||
Inverse Auswahl umkehren
|
||||
Invert Umkehren
|
||||
Invert Colors Farben umkehren
|
||||
@@ -3806,6 +3808,7 @@ theme Appearance Erscheinungsbild
|
||||
@msg select a gradient mesh wählen Sie ein Verlaufsgitter aus
|
||||
@msg select a linked image: an embedded one has no file wählen Sie ein verknüpftes Bild aus: Ein eingebettetes hat keine Datei
|
||||
@msg select a repeat wählen Sie eine Wiederholung aus
|
||||
@msg select an Intertwine group wählen Sie eine Verflechtungsgruppe aus
|
||||
@msg select a stroke with a variable width wählen Sie eine Kontur mit variabler Breite aus
|
||||
@msg select a symbol instance wählen Sie eine Symbolinstanz aus
|
||||
@msg select an Image Trace object wählen Sie ein Bildnachzeichner-Objekt aus
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1375,6 +1375,8 @@
|
||||
Internal CSS CSS interno
|
||||
Internet Standard RGB (sRGB) RGB estándar de Internet (sRGB)
|
||||
Intersect Intersecar
|
||||
Intertwine Entrelazar
|
||||
Arrange Intertwine Area Organizar área entrelazada
|
||||
Inverse Inversa
|
||||
Invert Invertir
|
||||
Invert Colors Invertir colores
|
||||
@@ -3902,6 +3904,7 @@ axis Both Ambos
|
||||
@msg select a gradient mesh selecciona una malla de degradado
|
||||
@msg select a linked image: an embedded one has no file selecciona una imagen vinculada: una incrustada no tiene archivo
|
||||
@msg select a repeat selecciona una repetición
|
||||
@msg select an Intertwine group selecciona un grupo entrelazado
|
||||
@msg select a stroke with a variable width selecciona un trazo con anchura variable
|
||||
@msg select a symbol instance selecciona una instancia de símbolo
|
||||
@msg select an Image Trace object selecciona un objeto de calco de imagen
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1374,6 +1374,8 @@
|
||||
Internal CSS CSS interne
|
||||
Internet Standard RGB (sRGB) RVB standard Internet (sRGB)
|
||||
Intersect Intersection
|
||||
Intertwine Entrelacer
|
||||
Arrange Intertwine Area Disposer la zone entrelacée
|
||||
Inverse Inverser la sélection
|
||||
Invert Inverser
|
||||
Invert Colors Inverser les couleurs
|
||||
@@ -3899,6 +3901,7 @@ axis Both Les deux
|
||||
@msg select a gradient mesh sélectionnez un filet de dégradé
|
||||
@msg select a linked image: an embedded one has no file sélectionnez une image liée : une image incorporée n’a pas de fichier
|
||||
@msg select a repeat sélectionnez une répétition
|
||||
@msg select an Intertwine group sélectionnez un groupe entrelacé
|
||||
@msg select a stroke with a variable width sélectionnez un contour de largeur variable
|
||||
@msg select a symbol instance sélectionnez une instance de symbole
|
||||
@msg select an Image Trace object sélectionnez un objet de vectorisation de l’image
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1746,6 +1746,8 @@
|
||||
Internet Standard RGB (sRGB) Internetes szabványos RGB (sRGB)
|
||||
Interpolation Interpoláció
|
||||
Intersect Metszet
|
||||
Intertwine Összefonás
|
||||
Arrange Intertwine Area Összefonási terület elrendezése
|
||||
Inverse Kijelölés megfordítása
|
||||
Invert Invertálás
|
||||
Invert Colors Színek invertálása
|
||||
@@ -4549,6 +4551,7 @@ imageTrace Outline Körvonal
|
||||
@msg select a linked image: an embedded one has no file jelölj ki egy csatolt képet: a beágyazottnak nincs fájlja
|
||||
@msg select a Live Paint group jelölj ki egy élőfestés-csoportot
|
||||
@msg select a repeat jelölj ki egy ismétlést
|
||||
@msg select an Intertwine group jelölj ki egy összefonási csoportot
|
||||
@msg select a stroke with a variable width jelölj ki egy változó szélességű körvonalat
|
||||
@msg select a symbol instance jelölj ki egy szimbólumpéldányt
|
||||
@msg select an envelope jelölj ki egy burkolót
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1375,6 +1375,8 @@
|
||||
Internal CSS CSS interno
|
||||
Internet Standard RGB (sRGB) RGB standard Internet (sRGB)
|
||||
Intersect Intersezione
|
||||
Intertwine Intreccia
|
||||
Arrange Intertwine Area Disponi area intrecciata
|
||||
Inverse Inversa
|
||||
Invert Inverti
|
||||
Invert Colors Inverti colori
|
||||
@@ -3905,6 +3907,7 @@ axis Both Entrambi
|
||||
@msg select a gradient mesh seleziona una trama sfumatura
|
||||
@msg select a linked image: an embedded one has no file seleziona un'immagine collegata: un'immagine incorporata non ha un file
|
||||
@msg select a repeat seleziona una ripetizione
|
||||
@msg select an Intertwine group seleziona un gruppo intrecciato
|
||||
@msg select a stroke with a variable width seleziona una traccia con larghezza variabile
|
||||
@msg select a symbol instance seleziona un'istanza di simbolo
|
||||
@msg select an Image Trace object seleziona un oggetto di ricalco immagine
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -426,6 +426,8 @@
|
||||
Pathfinder パスファインダー
|
||||
Add 追加
|
||||
Intersect 交差
|
||||
Intertwine 織り交ぜ
|
||||
Arrange Intertwine Area 織り交ぜ領域の重ね順
|
||||
Exclude 中マド
|
||||
Subtract 前面オブジェクトで型抜き
|
||||
Minus Back 背面オブジェクトで型抜き
|
||||
@@ -3750,6 +3752,7 @@ theme Appearance 外観
|
||||
@msg not in pattern editing mode パターン編集モードではありません
|
||||
@msg pattern missing パターンが見つかりません
|
||||
@msg select a repeat リピートを選択してください
|
||||
@msg select an Intertwine group 織り交ぜグループを選択してください
|
||||
@msg select the art to repeat リピートするアートを選択してください
|
||||
@msg the document has no patterns ドキュメントにパターンがありません
|
||||
@msg no artboard アートボードがありません
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -422,6 +422,8 @@
|
||||
Pathfinder 패스파인더
|
||||
Add 더하기
|
||||
Intersect 교차
|
||||
Intertwine 교차
|
||||
Arrange Intertwine Area 교차 영역 정렬
|
||||
Exclude 제외
|
||||
Subtract 빼기
|
||||
Minus Back 뒤 개체 빼기
|
||||
@@ -3728,6 +3730,7 @@ theme Appearance 외관
|
||||
@msg not in pattern editing mode 패턴 편집 모드가 아닙니다
|
||||
@msg pattern missing 패턴이 없습니다
|
||||
@msg select a repeat 반복을 선택하세요
|
||||
@msg select an Intertwine group 교차 그룹을 선택하세요
|
||||
@msg select the art to repeat 반복할 아트웍을 선택하세요
|
||||
@msg the document has no patterns 문서에 패턴이 없습니다
|
||||
@msg no artboard 아트보드가 없습니다
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -385,6 +385,8 @@
|
||||
Add Dodaj
|
||||
@id pathfinder.add Połącz
|
||||
Intersect Przetnij
|
||||
Intertwine Przeplatanie
|
||||
Arrange Intertwine Area Ułóż obszar przeplotu
|
||||
Exclude Wyklucz
|
||||
Subtract Odejmij
|
||||
Minus Back Odejmij tył
|
||||
@@ -4014,6 +4016,7 @@ imageTrace Outline Kontur
|
||||
@msg select a gradient mesh zaznacz siatkę gradientową
|
||||
@msg select a linked image: an embedded one has no file zaznacz obraz połączony: osadzony nie ma pliku
|
||||
@msg select a repeat zaznacz powtórzenie
|
||||
@msg select an Intertwine group zaznacz grupę przeplotu
|
||||
@msg select a stroke with a variable width zaznacz obrys o zmiennej szerokości
|
||||
@msg select a symbol instance zaznacz wystąpienie symbolu
|
||||
@msg select an Image Trace object zaznacz obiekt obrysu obrazu
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1175,6 +1175,8 @@
|
||||
Interlaced Entrelaçado
|
||||
Internet Standard RGB (sRGB) RGB Padrão da Internet (sRGB)
|
||||
Intersect Interseção
|
||||
Intertwine Entrelaçar
|
||||
Arrange Intertwine Area Organizar área entrelaçada
|
||||
Inverse Inverso
|
||||
Invert Inverter
|
||||
Invert Colors Inverter Cores
|
||||
@@ -3496,6 +3498,7 @@ axis Both Ambos
|
||||
@msg select a gradient mesh selecione uma malha de gradiente
|
||||
@msg select a linked image: an embedded one has no file selecione uma imagem vinculada: uma incorporada não tem arquivo
|
||||
@msg select a repeat selecione uma repetição
|
||||
@msg select an Intertwine group selecione um grupo entrelaçado
|
||||
@msg select a stroke with a variable width selecione um traço com largura variável
|
||||
@msg select a symbol instance selecione uma instância de símbolo
|
||||
@msg select an Image Trace object selecione um objeto de Vetorização de imagem
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -1373,6 +1373,8 @@
|
||||
Internal CSS Внутренний CSS
|
||||
Internet Standard RGB (sRGB) Стандартный интернет-RGB (sRGB)
|
||||
Intersect Пересечение
|
||||
Intertwine Переплетение
|
||||
Arrange Intertwine Area Упорядочить область переплетения
|
||||
Inverse Обратный
|
||||
Invert Инвертировать
|
||||
Invert Colors Инвертировать цвета
|
||||
@@ -3901,6 +3903,7 @@ axis Both Оба
|
||||
@msg select a gradient mesh выделите градиентную сетку
|
||||
@msg select a linked image: an embedded one has no file выделите связанное изображение: у внедрённого нет файла
|
||||
@msg select a repeat выделите повтор
|
||||
@msg select an Intertwine group выделите группу переплетения
|
||||
@msg select a stroke with a variable width выделите обводку с переменной шириной
|
||||
@msg select a symbol instance выделите экземпляр символа
|
||||
@msg select an Image Trace object выделите объект трассировки изображения
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1369,6 +1369,8 @@
|
||||
Internal CSS Внутрішній CSS
|
||||
Internet Standard RGB (sRGB) Стандартний інтернет-RGB (sRGB)
|
||||
Intersect Перетин
|
||||
Intertwine Переплетення
|
||||
Arrange Intertwine Area Упорядкувати ділянку переплетення
|
||||
Inverse Обернене
|
||||
Invert Інвертувати
|
||||
Invert Colors Інвертувати кольори
|
||||
@@ -3889,6 +3891,7 @@ axis Both Обидві
|
||||
@msg select a gradient mesh виділіть градієнтну сітку
|
||||
@msg select a linked image: an embedded one has no file виділіть пов’язане зображення: у вбудованого немає файлу
|
||||
@msg select a repeat виділіть повтор
|
||||
@msg select an Intertwine group виділіть групу переплетення
|
||||
@msg select a stroke with a variable width виділіть обведення зі змінною шириною
|
||||
@msg select a symbol instance виділіть екземпляр символу
|
||||
@msg select an Image Trace object виділіть об’єкт трасування зображення
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 718 and column 40.
|
@@ -1608,6 +1608,8 @@
|
||||
Internet Standard RGB (sRGB) RGB chuẩn Internet (sRGB)
|
||||
Interpolation Nội suy
|
||||
Intersect Giao nhau
|
||||
Intertwine Đan xen
|
||||
Arrange Intertwine Area Sắp xếp vùng đan xen
|
||||
Inverse Đảo ngược
|
||||
Invert Đảo ngược
|
||||
Invert Colors Đảo màu
|
||||
@@ -4182,6 +4184,7 @@ theme Appearance Giao diện
|
||||
@msg select a linked image: an embedded one has no file hãy chọn ảnh liên kết: ảnh nhúng không có tệp
|
||||
@msg select a Live Paint group hãy chọn nhóm tô trực tiếp
|
||||
@msg select a repeat hãy chọn một đối tượng lặp
|
||||
@msg select an Intertwine group hãy chọn một nhóm đan xen
|
||||
@msg select a stroke with a variable width hãy chọn nét có chiều rộng biến đổi
|
||||
@msg select a symbol instance hãy chọn đối tượng ký hiệu
|
||||
@msg select an envelope hãy chọn một Envelope
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 3726 and column 10.
|
@@ -1316,6 +1316,8 @@
|
||||
Internal CSS 内部 CSS
|
||||
Internet Standard RGB (sRGB) 互联网标准 RGB (sRGB)
|
||||
Intersect 交集
|
||||
Intertwine 交织
|
||||
Arrange Intertwine Area 排列交织区域
|
||||
Inverse 反向
|
||||
Invert 反相
|
||||
Invert Colors 反相颜色
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1346,6 +1346,8 @@
|
||||
Internal CSS 內部 CSS
|
||||
Internet Standard RGB (sRGB) 網際網路標準 RGB (sRGB)
|
||||
Intersect 交集
|
||||
Intertwine 交織
|
||||
Arrange Intertwine Area 排列交織區域
|
||||
Inverse 反轉
|
||||
Invert 反轉
|
||||
Invert Colors 反轉顏色
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -2377,6 +2377,10 @@ pub fn menu_tree_named(english_names: bool) -> Vec<(&'static str, Vec<Item>)> {
|
||||
c("Options…", "object.repeat.options"),
|
||||
],
|
||||
),
|
||||
sub(
|
||||
"Intertwine",
|
||||
vec![c("Make", "object.intertwine.make"), c("Edit", "object.intertwine.edit"), c("Release", "object.intertwine.release")],
|
||||
),
|
||||
sub(
|
||||
"Blend",
|
||||
vec![
|
||||
|
||||
+9
-2
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user