mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-02 22:50:27 -06:00
D: Pathfinder — add, subtract, intersect, exclude overlap, minus back (curves kept)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ea1a0b16c7
commit
9dd44f63cf
Generated
+84
-1
@@ -143,6 +143,12 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aliasable"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
|
||||
|
||||
[[package]]
|
||||
name = "alloc-no-stdlib"
|
||||
version = "2.0.4"
|
||||
@@ -976,6 +982,7 @@ dependencies = [
|
||||
name = "designcraft-geom"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"flo_curves",
|
||||
"kurbo",
|
||||
"proptest",
|
||||
"serde",
|
||||
@@ -1242,7 +1249,7 @@ dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"emath",
|
||||
"epaint",
|
||||
"itertools",
|
||||
"itertools 0.15.0",
|
||||
"log",
|
||||
"nohash-hasher",
|
||||
"profiling",
|
||||
@@ -1496,6 +1503,18 @@ dependencies = [
|
||||
"zlib-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "flo_curves"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5937462c05a3c43fecd2b0402b995b92888a35cc7dd6ca39306094c9b60a32fd"
|
||||
dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"ouroboros",
|
||||
"roots",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "float-cmp"
|
||||
version = "0.9.0"
|
||||
@@ -2021,6 +2040,12 @@ dependencies = [
|
||||
"pdf-writer",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.5.3"
|
||||
@@ -2201,6 +2226,15 @@ dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.15.0"
|
||||
@@ -3019,6 +3053,30 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ouroboros"
|
||||
version = "0.18.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e0f050db9c44b97a94723127e6be766ac5c340c48f2c4bb3ffa11713744be59"
|
||||
dependencies = [
|
||||
"aliasable",
|
||||
"ouroboros_macro",
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ouroboros_macro"
|
||||
version = "0.18.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c7028bdd3d43083f6d8d4d5187680d0d3560d54df4cc9d752005268b41e64d0"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"proc-macro2-diagnostics",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking"
|
||||
version = "2.2.1"
|
||||
@@ -3292,6 +3350,19 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2-diagnostics"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
"version_check",
|
||||
"yansi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "profiling"
|
||||
version = "1.0.18"
|
||||
@@ -3623,6 +3694,12 @@ dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "roots"
|
||||
version = "0.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "082f11ffa03bbef6c2c6ea6bea1acafaade2fd9050ae0234ab44a2153742b058"
|
||||
|
||||
[[package]]
|
||||
name = "roxmltree"
|
||||
version = "0.20.0"
|
||||
@@ -5476,6 +5553,12 @@ dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yansi"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.3"
|
||||
|
||||
@@ -50,6 +50,8 @@ image = { version = "0.25", default-features = false, features = ["png", "jpeg",
|
||||
resvg = { version = "0.47", default-features = false, features = ["text"] }
|
||||
krilla-svg = "0.8"
|
||||
psd = "0.3"
|
||||
# Boolean operations on Bézier paths (Pathfinder).
|
||||
flo_curves = "0.8"
|
||||
krilla = { version = "0.8", features = ["pdf"] }
|
||||
hayro = "0.7"
|
||||
quick-xml = "0.38"
|
||||
|
||||
@@ -30,6 +30,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
|
||||
- Edit › Paste Into (copied objects become a frame's content, clipped by it; rendered, PDF and IDML both ways — verified in InDesign 2026); Polygon Settings (sides, star inset; double-click the Polygon tool).
|
||||
- Color panel (fill/stroke proxy, CMYK/RGB/Lab sliders with channel ramps, tint slider for swatches, spectrum ramp, Add to Swatches) and Color Picker (double-click the proxy); mixed colours are unnamed (not listed in Swatches until added); `object.color`, `swatch.addToSwatches`, `swatch.addUnnamed`.
|
||||
- Place text files: Word (.docx: styles by name with their attributes, bold/italic/underline/size/font, footnotes, tables, tabs, breaks), RTF and plain text, into the insertion point, the selected frame or a new frame; autoflow adds pages and threaded frames until the text fits; Remove Styles option (`designcraft-textimport`).
|
||||
- Pathfinder (Object › Pathfinder and a Pathfinder panel): Add, Subtract, Intersect, Exclude Overlap, Minus Back; curves stay curves (flo_curves); `object.pathfinder`.
|
||||
- Make / Release Compound Path (⌘8; nested paths become holes), Create Outlines (⇧⌘O; one path per text colour), Scissors tool (C) with `path.split`.
|
||||
- Transform Again / Individually / Sequence Again (⌥⌘3; a moved copy repeats like Step and Repeat), Clear Transformations; Layers: Merge, Delete Unused, Hide/Lock Others, Show/Unlock All; Break Link to Style (paragraph and character).
|
||||
- Floating panels: tear a panel off the dock (its float button, or drag its header away) into a movable, resizable window; Dock puts it back; positions persist; `window.floatPanel` / `window.dockPanel`.
|
||||
|
||||
@@ -39,6 +39,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
has_selection,
|
||||
create_outlines
|
||||
),
|
||||
cmd!(
|
||||
"object.pathfinder",
|
||||
"Pathfinder",
|
||||
["Object", "Pathfinder"],
|
||||
None,
|
||||
"{op: add|subtract|intersect|exclude|minusBack, ids?} — combine the selected shapes into one (the backmost keeps its look; the frontmost for minusBack) → {id}",
|
||||
has_selection,
|
||||
pathfinder
|
||||
),
|
||||
cmd!(
|
||||
"path.split",
|
||||
"Split Path",
|
||||
@@ -104,6 +113,49 @@ fn make_compound(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
})
|
||||
}
|
||||
|
||||
fn pathfinder(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
use designcraft_geom::pathfinder::{Op, combine};
|
||||
let op_s = super::str_param(p, "op").unwrap_or("add");
|
||||
let op = Op::parse(op_s).ok_or_else(|| bad("object.pathfinder", format!("unknown op `{op_s}`")))?;
|
||||
let ids = targets(s, p)?;
|
||||
if ids.len() < 2 {
|
||||
return Err(bad("object.pathfinder", "select two or more shapes"));
|
||||
}
|
||||
s.edit(|d, sel| {
|
||||
let (sr, _) = spread_xf(d, ids[0]).ok_or(designcraft_doc::DocError::NoItem(ids[0]))?;
|
||||
let order: Vec<ItemId> = d
|
||||
.spread(sr)
|
||||
.map(|sp| {
|
||||
sp.items
|
||||
.iter()
|
||||
.filter(|i| ids.contains(&i.id) && !matches!(i.content, Content::Group { .. } | Content::Text(_)))
|
||||
.map(|i| i.id)
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
if order.len() < 2 {
|
||||
return Err(bad("object.pathfinder", "select two or more shapes on one spread (not text frames or groups)"));
|
||||
}
|
||||
let keep = if op == Op::MinusBack { *order.last().expect("two") } else { order[0] };
|
||||
let shapes: Vec<PathData> =
|
||||
order.iter().filter_map(|id| spread_xf(d, *id).and_then(|(_, xf)| Some(d.item(*id)?.path.transformed(xf)))).collect();
|
||||
let Some(result) = combine(op, &shapes) else { return Err(bad("object.pathfinder", "the shapes don't overlap: nothing is left")) };
|
||||
let (_, kxf) = spread_xf(d, keep).ok_or(designcraft_doc::DocError::NoItem(keep))?;
|
||||
for id in &order {
|
||||
if *id != keep {
|
||||
d.remove_item(*id)?;
|
||||
}
|
||||
}
|
||||
let it = d.item_mut(keep).ok_or(designcraft_doc::DocError::NoItem(keep))?;
|
||||
it.path = result.transformed(kxf.inverse());
|
||||
orient_holes(&mut it.path);
|
||||
it.shape = Shape::Path;
|
||||
it.corners = Default::default();
|
||||
*sel = Selection::items(vec![keep]);
|
||||
Ok(json!({"id": keep.0}))
|
||||
})
|
||||
}
|
||||
|
||||
fn release_compound(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let ids = targets(s, p)?;
|
||||
s.edit(|d, sel| {
|
||||
@@ -302,6 +354,22 @@ mod tests {
|
||||
s.doc().unwrap().doc.check().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pathfinder_subtracts_front_from_back() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
let a = s.execute("frame.create", &json!({"rect": [100, 100, 300, 300]})).unwrap()["id"].as_u64().unwrap();
|
||||
let b = s.execute("frame.create", &json!({"rect": [200, 100, 400, 300], "shape": "ellipse"})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("selection.set", &json!({"ids": [a, b]})).unwrap();
|
||||
let r = s.execute("object.pathfinder", &json!({"op": "subtract"})).unwrap();
|
||||
assert_eq!(r["id"], a);
|
||||
let d = s.doc().unwrap().doc.clone();
|
||||
assert!(d.item(ItemId(b)).is_none());
|
||||
let bb = d.item(ItemId(a)).unwrap().bounds();
|
||||
assert!((bb.x1 - 300.0).abs() < 0.5 && (bb.x0 - 100.0).abs() < 0.5, "{bb:?}");
|
||||
assert!(s.execute("object.pathfinder", &json!({"op": "nope"})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_outlines_turns_text_into_paths() {
|
||||
let mut s = Session::new();
|
||||
|
||||
@@ -10,6 +10,7 @@ workspace = true
|
||||
|
||||
[dependencies]
|
||||
kurbo = { workspace = true }
|
||||
flo_curves = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
pub mod corners;
|
||||
pub mod hit;
|
||||
pub mod path;
|
||||
pub mod pathfinder;
|
||||
pub mod shapes;
|
||||
pub mod snap;
|
||||
pub mod units;
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
//! Pathfinder: Add, Subtract, Intersect, Exclude Overlap and Minus Back on closed paths, keeping
|
||||
//! curves as curves (flo_curves' Bézier path arithmetic).
|
||||
|
||||
use flo_curves::bezier::path::{SimpleBezierPath, path_add, path_intersect, path_sub};
|
||||
use flo_curves::geo::Coord2;
|
||||
|
||||
use crate::Point;
|
||||
use crate::path::{Anchor, AnchorKind, PathData, SubPath};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Op {
|
||||
/// Union of all shapes.
|
||||
Add,
|
||||
/// The front shapes cut out of the backmost one.
|
||||
Subtract,
|
||||
/// The area all shapes share.
|
||||
Intersect,
|
||||
/// Areas covered an odd number of times.
|
||||
Exclude,
|
||||
/// The back shapes cut out of the frontmost one.
|
||||
MinusBack,
|
||||
}
|
||||
|
||||
impl Op {
|
||||
pub fn parse(s: &str) -> Option<Op> {
|
||||
Some(match s {
|
||||
"add" => Op::Add,
|
||||
"subtract" => Op::Subtract,
|
||||
"intersect" => Op::Intersect,
|
||||
"exclude" => Op::Exclude,
|
||||
"minusBack" => Op::MinusBack,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const ACCURACY: f64 = 0.01;
|
||||
|
||||
fn c(p: Point) -> Coord2 {
|
||||
Coord2(p.x, p.y)
|
||||
}
|
||||
|
||||
/// Every subpath as a closed Bézier path (open ones are closed with a straight segment).
|
||||
fn to_flo(path: &PathData) -> Vec<SimpleBezierPath> {
|
||||
let mut out = Vec::new();
|
||||
for sp in &path.subpaths {
|
||||
if sp.anchors.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
let mut closed = sp.clone();
|
||||
closed.closed = true;
|
||||
let curves = (0..closed.segment_count()).map(|i| {
|
||||
let s = closed.segment(i);
|
||||
(c(s.p1), c(s.p2), c(s.p3))
|
||||
});
|
||||
out.push((c(closed.anchors[0].p), curves.collect()));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn from_flo(paths: &[SimpleBezierPath]) -> PathData {
|
||||
let p = |c: &Coord2| Point::new(c.0, c.1);
|
||||
let mut subpaths = Vec::new();
|
||||
for (start, curves) in paths {
|
||||
if curves.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let mut anchors = vec![Anchor::corner(p(start))];
|
||||
for (c1, c2, end) in curves {
|
||||
anchors.last_mut().expect("non-empty").h_out = p(c1);
|
||||
let mut a = Anchor::corner(p(end));
|
||||
a.h_in = p(c2);
|
||||
anchors.push(a);
|
||||
}
|
||||
// The last curve comes back to the start: fold its end into the first anchor.
|
||||
if anchors.len() > 2 && anchors.last().expect("non-empty").p.distance(anchors[0].p) < 1e-6 {
|
||||
let last = anchors.pop().expect("non-empty");
|
||||
anchors[0].h_in = last.h_in;
|
||||
}
|
||||
for a in &mut anchors {
|
||||
*a = Anchor::with_handles(a.p, a.h_in, a.h_out);
|
||||
if !a.has_in() && !a.has_out() {
|
||||
a.kind = AnchorKind::Corner;
|
||||
}
|
||||
}
|
||||
subpaths.push(SubPath::new(anchors, true));
|
||||
}
|
||||
PathData::new(subpaths)
|
||||
}
|
||||
|
||||
/// Combine `shapes` (back to front, all in one coordinate space). `None` when nothing is left.
|
||||
pub fn combine(op: Op, shapes: &[PathData]) -> Option<PathData> {
|
||||
let flo: Vec<Vec<SimpleBezierPath>> = shapes.iter().map(to_flo).filter(|p| !p.is_empty()).collect();
|
||||
let (first, rest) = flo.split_first()?;
|
||||
let union = |v: &[Vec<SimpleBezierPath>]| -> Vec<SimpleBezierPath> {
|
||||
let mut acc: Vec<SimpleBezierPath> = Vec::new();
|
||||
for p in v {
|
||||
acc = if acc.is_empty() { p.clone() } else { path_add(&acc, p, ACCURACY) };
|
||||
}
|
||||
acc
|
||||
};
|
||||
let out: Vec<SimpleBezierPath> = match op {
|
||||
Op::Add => union(&flo),
|
||||
Op::Subtract => {
|
||||
let cut = union(rest);
|
||||
if cut.is_empty() { first.clone() } else { path_sub(first, &cut, ACCURACY) }
|
||||
}
|
||||
Op::MinusBack => {
|
||||
let (front, backs) = flo.split_last()?;
|
||||
let cut = union(backs);
|
||||
if cut.is_empty() { front.clone() } else { path_sub(front, &cut, ACCURACY) }
|
||||
}
|
||||
Op::Intersect => {
|
||||
let mut acc = first.clone();
|
||||
for p in rest {
|
||||
acc = path_intersect(&acc, p, ACCURACY);
|
||||
if acc.is_empty() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
acc
|
||||
}
|
||||
Op::Exclude => {
|
||||
let mut acc = first.clone();
|
||||
for p in rest {
|
||||
let a: Vec<SimpleBezierPath> = path_sub(&acc, p, ACCURACY);
|
||||
let b: Vec<SimpleBezierPath> = path_sub(p, &acc, ACCURACY);
|
||||
acc = a.into_iter().chain(b).collect();
|
||||
}
|
||||
acc
|
||||
}
|
||||
};
|
||||
let r = from_flo(&out);
|
||||
if r.subpaths.is_empty() { None } else { Some(r) }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{Rect, shapes};
|
||||
|
||||
fn area(p: &PathData) -> f64 {
|
||||
p.subpaths.iter().map(|s| s.area()).sum::<f64>().abs()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn boolean_ops_on_overlapping_squares() {
|
||||
let a = shapes::rectangle(Rect::new(0.0, 0.0, 100.0, 100.0));
|
||||
let b = shapes::rectangle(Rect::new(50.0, 50.0, 150.0, 150.0));
|
||||
let near = |x: f64, y: f64| (x - y).abs() < 1.0;
|
||||
let add = combine(Op::Add, &[a.clone(), b.clone()]).unwrap();
|
||||
assert!(near(area(&add), 17500.0), "add {}", area(&add));
|
||||
let sub = combine(Op::Subtract, &[a.clone(), b.clone()]).unwrap();
|
||||
assert!(near(area(&sub), 7500.0), "subtract {}", area(&sub));
|
||||
let int = combine(Op::Intersect, &[a.clone(), b.clone()]).unwrap();
|
||||
assert!(near(area(&int), 2500.0), "intersect {}", area(&int));
|
||||
assert_eq!(int.bounds().unwrap(), Rect::new(50.0, 50.0, 100.0, 100.0));
|
||||
let mb = combine(Op::MinusBack, &[a.clone(), b.clone()]).unwrap();
|
||||
assert!(near(area(&mb), 7500.0));
|
||||
assert!(mb.bounds().unwrap().x1 > 149.0, "the front square remains");
|
||||
let ex = combine(Op::Exclude, &[a.clone(), b]).unwrap();
|
||||
assert_eq!(ex.subpaths.len(), 2);
|
||||
let far = shapes::rectangle(Rect::new(500.0, 500.0, 510.0, 510.0));
|
||||
assert!(combine(Op::Intersect, &[a, far]).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn curves_stay_curves() {
|
||||
let a = shapes::ellipse(Rect::new(0.0, 0.0, 100.0, 100.0));
|
||||
let b = shapes::ellipse(Rect::new(60.0, 0.0, 160.0, 100.0));
|
||||
let add = combine(Op::Add, &[a, b]).unwrap();
|
||||
assert_eq!(add.subpaths.len(), 1);
|
||||
assert!(add.anchors().any(|(_, _, a)| a.has_in() || a.has_out()), "curved anchors");
|
||||
let bb = add.bounds().unwrap();
|
||||
assert!((bb.x0 - 0.0).abs() < 0.5 && (bb.x1 - 160.0).abs() < 0.5, "{bb:?}");
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ pub const ICON_PANELS: &[(&str, &str, &str)] = &[
|
||||
("textWrap", "Text Wrap", "panel-wrap"),
|
||||
("table", "Table", "panel-table"),
|
||||
("align", "Align", "panel-align"),
|
||||
("pathfinder", "Pathfinder", "tool-scissors"),
|
||||
("links", "Links", "panel-links"),
|
||||
("info", "Info", "panel-info"),
|
||||
("preflight", "Preflight", "panel-preflight"),
|
||||
@@ -154,6 +155,7 @@ pub fn panel_body(app: &mut DesignApp, ui: &mut egui::Ui, id: &str) {
|
||||
"paragraph" => panels::properties::paragraph_panel(app, ui),
|
||||
"textWrap" => panels::properties::wrap_panel(app, ui),
|
||||
"align" => panels::properties::align_panel(app, ui),
|
||||
"pathfinder" => panels::properties::pathfinder_panel(app, ui),
|
||||
"color" => panels::swatches::color_panel(app, ui),
|
||||
"gradient" => panels::swatches::gradient_panel(app, ui),
|
||||
"effects" => panels::properties::effects_panel(app, ui),
|
||||
|
||||
@@ -286,6 +286,13 @@ pub const MENUS: &[(&str, &[&str])] = &[
|
||||
"cmd:object.makeCompoundPath",
|
||||
"cmd:object.releaseCompoundPath",
|
||||
"<",
|
||||
">Pathfinder",
|
||||
"cmd:object.pathfinder|Add|{\"op\": \"add\"}",
|
||||
"cmd:object.pathfinder|Subtract|{\"op\": \"subtract\"}",
|
||||
"cmd:object.pathfinder|Intersect|{\"op\": \"intersect\"}",
|
||||
"cmd:object.pathfinder|Exclude Overlap|{\"op\": \"exclude\"}",
|
||||
"cmd:object.pathfinder|Minus Back|{\"op\": \"minusBack\"}",
|
||||
"<",
|
||||
">Arrange",
|
||||
"cmd:object.bringToFront",
|
||||
"cmd:object.bringForward",
|
||||
|
||||
@@ -1445,6 +1445,30 @@ pub fn wrap_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
});
|
||||
}
|
||||
|
||||
/// Pathfinder panel: combine the selected shapes; make or release compound paths.
|
||||
pub fn pathfinder_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
ui.label(egui::RichText::new("Pathfinder").strong());
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
for (label, op) in
|
||||
[("Add", "add"), ("Subtract", "subtract"), ("Intersect", "intersect"), ("Exclude Overlap", "exclude"), ("Minus Back", "minusBack")]
|
||||
{
|
||||
if ui.button(label).clicked() {
|
||||
let _ = app.run("object.pathfinder", json!({"op": op}));
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
ui.label(egui::RichText::new("Paths").strong());
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("Make Compound").clicked() {
|
||||
let _ = app.run("object.makeCompoundPath", json!({}));
|
||||
}
|
||||
if ui.button("Release").clicked() {
|
||||
let _ = app.run("object.releaseCompoundPath", json!({}));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn align_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
ui.horizontal(|ui| {
|
||||
for (edge, tip) in [
|
||||
|
||||
+3
-3
@@ -10,7 +10,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
|
||||
| Priority | Features | Done | Partial | Missing | Parity |
|
||||
|---|---|---|---|---|---|
|
||||
| P0 | 102 | 99 | 3 | 0 | 99% |
|
||||
| P1 | 97 | 42 | 23 | 32 | 55% |
|
||||
| P1 | 97 | 43 | 23 | 31 | 56% |
|
||||
| P2 | 76 | 8 | 7 | 61 | 15% |
|
||||
|
||||
| Area | Parity |
|
||||
@@ -18,7 +18,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
|
||||
| Application shell & workspace | 69% |
|
||||
| Documents, pages, spreads | 71% |
|
||||
| Layers | 95% |
|
||||
| Frames, shapes & paths | 83% |
|
||||
| Frames, shapes & paths | 85% |
|
||||
| Transform | 93% |
|
||||
| Fill, stroke, colour | 69% |
|
||||
| Effects & transparency | 50% |
|
||||
@@ -104,7 +104,7 @@ Open P0 items: Preferences, Vector/page formats (PDF/AI/SVG/EPS place), Export t
|
||||
| Frames, shapes & paths | Content type | P0 | D |
|
||||
| Frames, shapes & paths | Convert Shape | P1 | P |
|
||||
| Frames, shapes & paths | Corner Options | P0 | D |
|
||||
| Frames, shapes & paths | Pathfinder | P1 | M |
|
||||
| Frames, shapes & paths | Pathfinder | P1 | D |
|
||||
| Frames, shapes & paths | Compound paths | P1 | D |
|
||||
| Frames, shapes & paths | Text outlines | P1 | D |
|
||||
| Frames, shapes & paths | Clipping paths | P2 | M |
|
||||
|
||||
Reference in New Issue
Block a user