D: Add/Delete Anchor Point and Convert Direction Point tools (path.addAnchor/deleteAnchor/convertAnchor)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
66ab252044
commit
349525bae8
|
|
@ -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`).
|
||||
- Add Anchor Point (=), Delete Anchor Point (-) and Convert Direction Point (⇧C) tools (click toggles smooth/corner, drag pulls out handles), with `path.addAnchor`, `path.deleteAnchor`, `path.convertAnchor`.
|
||||
- Gradients: the Gradient Swatch tool (G) drags a gradient's start and end across objects (Shift: 45°); the Gradient panel sets type, angle, reverse and edits stops on a ramp (drag, add, drag off to remove, location, colour). Edited gradients are unnamed swatches; `object.gradient`; the vector round-trips through IDML.
|
||||
- Document Setup (⌥⌘P): intent, number of pages, start page # (an even start begins with a left page), facing pages, size presets and orientation, bleed and slug per edge; `layout.documentSetup {}` reports the setup.
|
||||
- Stroke panel: cap, join, miter limit, alignment, type, Start/End arrowheads (12 kinds, drawn on screen and in PDF; the path is shortened under pointed heads) and gap colour under dashes and dots.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,220 @@
|
|||
//! Anchor-point editing (the Pen tool group): Add Anchor Point, Delete Anchor Point and Convert
|
||||
//! Direction Point. Positions are in spread coordinates; anchors are `(subpath, index)`.
|
||||
|
||||
use designcraft_doc::{Document, ItemId, Shape};
|
||||
use designcraft_geom::{Affine, Anchor, AnchorKind, Point};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::{CommandSpec, bad, cmd, has_doc, point_param};
|
||||
use crate::{Result, Session};
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
cmd!(
|
||||
"path.addAnchor",
|
||||
"Add Anchor Point",
|
||||
[],
|
||||
None,
|
||||
"{id, at: [x,y] (spread coords; the nearest point on the path), tolerance?: pt (default 4)} → {subpath, anchor}",
|
||||
has_doc,
|
||||
add_anchor
|
||||
),
|
||||
cmd!(
|
||||
"path.deleteAnchor",
|
||||
"Delete Anchor Point",
|
||||
[],
|
||||
None,
|
||||
"{id, subpath, anchor} — removes the point (and the object when no path is left)",
|
||||
has_doc,
|
||||
delete_anchor
|
||||
),
|
||||
cmd!(
|
||||
"path.convertAnchor",
|
||||
"Convert Direction Point",
|
||||
[],
|
||||
None,
|
||||
"{id, subpath, anchor, to?: [x,y] (spread coords: drag out symmetric handles to there), corner?: bool} — without `to`, a smooth point becomes a corner and a corner gets smooth handles",
|
||||
has_doc,
|
||||
convert_anchor
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
/// Spread → item-space transform of `id`.
|
||||
fn to_inner(d: &Document, id: ItemId) -> Option<Affine> {
|
||||
let loc = d.find(id)?;
|
||||
Some((d.parent_xf(&loc) * d.item_at(&loc)?.xf).inverse())
|
||||
}
|
||||
|
||||
fn anchor_param(p: &Value, cmd: &str) -> Result<(ItemId, usize, usize)> {
|
||||
let id = super::id_param(p, "id").ok_or_else(|| bad(cmd, "missing id"))?;
|
||||
let si = p.get("subpath").and_then(Value::as_u64).unwrap_or(0) as usize;
|
||||
let ai = p.get("anchor").and_then(Value::as_u64).ok_or_else(|| bad(cmd, "missing anchor"))? as usize;
|
||||
Ok((id, si, ai))
|
||||
}
|
||||
|
||||
/// Editing points makes a drawn rectangle/ellipse/polygon an ordinary path.
|
||||
fn as_path(shape: &mut Shape) {
|
||||
if matches!(shape, Shape::Rectangle | Shape::Oval | Shape::Polygon) {
|
||||
*shape = Shape::Path;
|
||||
}
|
||||
}
|
||||
|
||||
fn add_anchor(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let id = super::id_param(p, "id").ok_or_else(|| bad("path.addAnchor", "missing id"))?;
|
||||
let at = point_param(p, "at").ok_or_else(|| bad("path.addAnchor", "missing at"))?;
|
||||
let tol = p.get("tolerance").and_then(Value::as_f64).unwrap_or(4.0);
|
||||
s.edit(|d, _| {
|
||||
let inv = to_inner(d, id).ok_or(designcraft_doc::DocError::NoItem(id))?;
|
||||
let it = d.item_mut(id).ok_or(designcraft_doc::DocError::NoItem(id))?;
|
||||
let q = inv * at;
|
||||
let (si, seg, t, _, dist) = it.path.nearest(q).ok_or_else(|| bad("path.addAnchor", "the object has no path"))?;
|
||||
// Tolerance is in spread units; the item space may be scaled.
|
||||
let scale = inv.determinant().abs().sqrt().max(1e-9);
|
||||
if dist > tol * scale {
|
||||
return Err(bad("path.addAnchor", "not on the path"));
|
||||
}
|
||||
let t = t.clamp(1e-4, 1.0 - 1e-4);
|
||||
let ai = it.path.subpaths[si].insert_anchor(seg, t);
|
||||
as_path(&mut it.shape);
|
||||
Ok(json!({"subpath": si, "anchor": ai}))
|
||||
})
|
||||
}
|
||||
|
||||
fn delete_anchor(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let (id, si, ai) = anchor_param(p, "path.deleteAnchor")?;
|
||||
s.edit(|d, sel| {
|
||||
let it = d.item_mut(id).ok_or(designcraft_doc::DocError::NoItem(id))?;
|
||||
let sp = it.path.subpaths.get_mut(si).ok_or_else(|| bad("path.deleteAnchor", "no such subpath"))?;
|
||||
if ai >= sp.anchors.len() {
|
||||
return Err(bad("path.deleteAnchor", "no such anchor"));
|
||||
}
|
||||
sp.anchors.remove(ai);
|
||||
// A subpath needs two points (three to stay closed).
|
||||
if sp.anchors.len() < 2 {
|
||||
it.path.subpaths.remove(si);
|
||||
} else if sp.closed && sp.anchors.len() < 3 {
|
||||
sp.closed = false;
|
||||
}
|
||||
as_path(&mut it.shape);
|
||||
if it.path.subpaths.is_empty() {
|
||||
d.remove_item(id)?;
|
||||
sel.items.retain(|i| *i != id);
|
||||
return Ok(json!({"deleted": true}));
|
||||
}
|
||||
Ok(json!({"deleted": false}))
|
||||
})
|
||||
}
|
||||
|
||||
fn convert_anchor(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let (id, si, ai) = anchor_param(p, "path.convertAnchor")?;
|
||||
let to = point_param(p, "to");
|
||||
let corner = p.get("corner").and_then(Value::as_bool);
|
||||
s.edit(|d, _| {
|
||||
let inv = to_inner(d, id).ok_or(designcraft_doc::DocError::NoItem(id))?;
|
||||
let it = d.item_mut(id).ok_or(designcraft_doc::DocError::NoItem(id))?;
|
||||
let sp = it.path.subpaths.get_mut(si).ok_or_else(|| bad("path.convertAnchor", "no such subpath"))?;
|
||||
let n = sp.anchors.len();
|
||||
if ai >= n {
|
||||
return Err(bad("path.convertAnchor", "no such anchor"));
|
||||
}
|
||||
let closed = sp.closed;
|
||||
let a = sp.anchors[ai];
|
||||
let has_handles = a.has_in() || a.has_out();
|
||||
let new = match (to, corner) {
|
||||
(Some(t), _) => Anchor::smooth(a.p, inv * t),
|
||||
(None, Some(true)) => Anchor::corner(a.p),
|
||||
(None, Some(false)) => smooth_auto(&sp.anchors, ai, closed),
|
||||
(None, None) if has_handles => Anchor::corner(a.p),
|
||||
(None, None) => smooth_auto(&sp.anchors, ai, closed),
|
||||
};
|
||||
sp.anchors[ai] = new;
|
||||
as_path(&mut it.shape);
|
||||
Ok(json!({"kind": if new.kind == AnchorKind::Smooth { "smooth" } else { "corner" }}))
|
||||
})
|
||||
}
|
||||
|
||||
/// Smooth handles for a corner: parallel to the line through its neighbours, a third of the way
|
||||
/// to each.
|
||||
fn smooth_auto(anchors: &[Anchor], i: usize, closed: bool) -> Anchor {
|
||||
let n = anchors.len();
|
||||
let p = anchors[i].p;
|
||||
let prev = if i > 0 {
|
||||
Some(anchors[i - 1].p)
|
||||
} else if closed {
|
||||
Some(anchors[n - 1].p)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let next = if i + 1 < n {
|
||||
Some(anchors[i + 1].p)
|
||||
} else if closed {
|
||||
Some(anchors[0].p)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let (a, b) = (prev.unwrap_or(p), next.unwrap_or(p));
|
||||
let dir = b - a;
|
||||
let len = dir.hypot();
|
||||
if len < 1e-9 {
|
||||
return anchors[i];
|
||||
}
|
||||
let u = dir / len;
|
||||
let (lin, lout) = ((p - a).hypot() / 3.0, (b - p).hypot() / 3.0);
|
||||
let lin = if prev.is_some() { lin } else { lout };
|
||||
let lout = if next.is_some() { lout } else { lin };
|
||||
Anchor { p, h_in: p - u * lin, h_out: p + u * lout, kind: AnchorKind::Smooth }
|
||||
}
|
||||
|
||||
/// The anchor of item `id` nearest to `at` (spread coords), within `tol`.
|
||||
pub fn anchor_near(d: &Document, id: ItemId, at: Point, tol: f64) -> Option<(usize, usize)> {
|
||||
let loc = d.find(id)?;
|
||||
let m = d.parent_xf(&loc) * d.item_at(&loc)?.xf;
|
||||
d.item(id)?
|
||||
.path
|
||||
.anchors()
|
||||
.map(|(si, ai, a)| (si, ai, ((m * a.p) - at).hypot()))
|
||||
.filter(|x| x.2 <= tol)
|
||||
.min_by(|a, b| a.2.total_cmp(&b.2))
|
||||
.map(|x| (x.0, x.1))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn add_convert_delete_anchor_points() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
let id = ItemId(s.execute("frame.create", &json!({"rect": [100, 100, 200, 200]})).unwrap()["id"].as_u64().unwrap());
|
||||
let count = |s: &Session| s.doc().unwrap().doc.item(id).unwrap().path.anchor_count();
|
||||
assert_eq!(count(&s), 4);
|
||||
assert!(s.execute("path.addAnchor", &json!({"id": id.0, "at": [150, 150]})).is_err(), "not on the path");
|
||||
let r = s.execute("path.addAnchor", &json!({"id": id.0, "at": [150, 101]})).unwrap();
|
||||
assert_eq!(count(&s), 5);
|
||||
let ai = r["anchor"].as_u64().unwrap() as usize;
|
||||
let d = s.doc().unwrap().doc.clone();
|
||||
let it = d.item(id).unwrap();
|
||||
assert_eq!(it.shape, Shape::Path);
|
||||
assert!((it.path.subpaths[0].anchors[ai].p.y - 100.0).abs() < 1e-6);
|
||||
assert_eq!(anchor_near(&d, id, Point::new(150.5, 100.5), 3.0), Some((0, ai)));
|
||||
// Drag out handles, then click again to make it a corner.
|
||||
s.execute("path.convertAnchor", &json!({"id": id.0, "anchor": ai, "to": [170, 90]})).unwrap();
|
||||
let a = s.doc().unwrap().doc.item(id).unwrap().path.subpaths[0].anchors[ai];
|
||||
assert_eq!(a.kind, AnchorKind::Smooth);
|
||||
assert!((a.h_out - Point::new(170.0, 90.0)).hypot() < 1e-9 && (a.h_in - Point::new(130.0, 110.0)).hypot() < 1e-9);
|
||||
assert_eq!(s.execute("path.convertAnchor", &json!({"id": id.0, "anchor": ai})).unwrap()["kind"], "corner");
|
||||
assert_eq!(s.execute("path.convertAnchor", &json!({"id": id.0, "anchor": 0})).unwrap()["kind"], "smooth");
|
||||
s.execute("path.deleteAnchor", &json!({"id": id.0, "anchor": ai})).unwrap();
|
||||
assert_eq!(count(&s), 4);
|
||||
// Deleting down to one point removes the object.
|
||||
for _ in 0..2 {
|
||||
s.execute("path.deleteAnchor", &json!({"id": id.0, "anchor": 0})).unwrap();
|
||||
}
|
||||
assert!(!s.doc().unwrap().doc.item(id).unwrap().path.is_closed(), "two points can't stay closed");
|
||||
let r = s.execute("path.deleteAnchor", &json!({"id": id.0, "anchor": 0})).unwrap();
|
||||
assert_eq!(r["deleted"], true);
|
||||
assert!(s.doc().unwrap().doc.item(id).is_none());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
//! The command registry. Ids follow InDesign's menu structure.
|
||||
|
||||
mod anchored;
|
||||
pub mod anchors;
|
||||
mod datamerge;
|
||||
mod edit;
|
||||
mod export;
|
||||
|
|
@ -124,6 +125,7 @@ pub fn command_specs() -> &'static [CommandSpec] {
|
|||
v.extend(export::specs());
|
||||
v.extend(edit::specs());
|
||||
v.extend(object::specs());
|
||||
v.extend(anchors::specs());
|
||||
v.extend(text::specs());
|
||||
v.extend(style::specs());
|
||||
v.extend(table::specs());
|
||||
|
|
|
|||
|
|
@ -0,0 +1,102 @@
|
|||
//! Add Anchor Point (=), Delete Anchor Point (-) and Convert Direction Point (Shift+C): they act on
|
||||
//! the selected paths, or the path under the pointer when nothing is selected.
|
||||
|
||||
use designcraft_doc::ItemId;
|
||||
use designcraft_geom::Point;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::select::anchor_at_in;
|
||||
use crate::{Action, Cursor, Mods, PointerEvent, PointerKind, Tool, ToolContext};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Kind {
|
||||
Add,
|
||||
Delete,
|
||||
Convert,
|
||||
}
|
||||
|
||||
pub struct AnchorTool {
|
||||
kind: Kind,
|
||||
/// Convert: the anchor pressed on (id, subpath, anchor), the press point, whether it dragged.
|
||||
press: Option<(u64, usize, usize, Point, bool)>,
|
||||
}
|
||||
|
||||
impl AnchorTool {
|
||||
pub fn new(kind: Kind) -> Self {
|
||||
Self { kind, press: None }
|
||||
}
|
||||
}
|
||||
|
||||
/// The paths the tool works on.
|
||||
fn targets(cx: &ToolContext, p: Point) -> Vec<ItemId> {
|
||||
if cx.selection.items.is_empty() { cx.hit(p).map(|(_, id)| vec![id]).unwrap_or_default() } else { cx.selection.items.clone() }
|
||||
}
|
||||
|
||||
impl Tool for AnchorTool {
|
||||
fn id(&self) -> &'static str {
|
||||
match self.kind {
|
||||
Kind::Add => "addAnchor",
|
||||
Kind::Delete => "deleteAnchor",
|
||||
Kind::Convert => "convertDirection",
|
||||
}
|
||||
}
|
||||
|
||||
fn pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec<Action> {
|
||||
let p = ev.pos;
|
||||
match (self.kind, ev.kind) {
|
||||
(Kind::Add, PointerKind::Down) => {
|
||||
let Some((_, sp)) = cx.layout.spread_at(p) else { return vec![] };
|
||||
// The first target whose outline passes near the pointer.
|
||||
for id in targets(cx, p) {
|
||||
let Some(it) = cx.doc.item(id) else { continue };
|
||||
let Some(xf) = cx.item_canvas_xf(id) else { continue };
|
||||
let inner = (xf * it.xf).inverse() * p;
|
||||
let scale = (xf * it.xf).inverse().determinant().abs().sqrt();
|
||||
if it.path.nearest(inner).is_some_and(|n| n.4 <= cx.tol(5.0) * scale) {
|
||||
return vec![Action::Exec("path.addAnchor".into(), json!({"id": id.0, "at": [sp.x, sp.y], "tolerance": cx.tol(5.0)}))];
|
||||
}
|
||||
}
|
||||
vec![]
|
||||
}
|
||||
(Kind::Delete, PointerKind::Down) => match anchor_at_in(cx, &targets(cx, p), p) {
|
||||
Some((id, si, ai, None)) => vec![Action::Exec("path.deleteAnchor".into(), json!({"id": id, "subpath": si, "anchor": ai}))],
|
||||
_ => vec![],
|
||||
},
|
||||
(Kind::Convert, PointerKind::Down) => {
|
||||
if let Some((id, si, ai, None)) = anchor_at_in(cx, &targets(cx, p), p) {
|
||||
self.press = Some((id, si, ai, p, false));
|
||||
}
|
||||
vec![]
|
||||
}
|
||||
(Kind::Convert, PointerKind::Drag) => {
|
||||
let Some((id, si, ai, start, dragged)) = self.press else { return vec![] };
|
||||
if !dragged && (p - start).hypot() < cx.tol(3.0) {
|
||||
return vec![];
|
||||
}
|
||||
let Some((_, sp)) = cx.layout.spread_at(p) else { return vec![] };
|
||||
let mut out = vec![];
|
||||
if !dragged {
|
||||
out.push(Action::Begin("Convert Direction Point".into()));
|
||||
self.press = Some((id, si, ai, start, true));
|
||||
}
|
||||
out.push(Action::Preview("path.convertAnchor".into(), json!({"id": id, "subpath": si, "anchor": ai, "to": [sp.x, sp.y]})));
|
||||
out
|
||||
}
|
||||
(Kind::Convert, PointerKind::Up) => match self.press.take() {
|
||||
Some((_, _, _, _, true)) => vec![Action::Commit],
|
||||
// A click toggles smooth ↔ corner.
|
||||
Some((id, si, ai, _, false)) => vec![Action::Exec("path.convertAnchor".into(), json!({"id": id, "subpath": si, "anchor": ai}))],
|
||||
None => vec![],
|
||||
},
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn cursor(&self, _cx: &ToolContext, _p: Point, _m: Mods) -> Cursor {
|
||||
Cursor::Pen
|
||||
}
|
||||
|
||||
fn busy(&self) -> bool {
|
||||
self.press.is_some_and(|p| p.4)
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@
|
|||
//! Pointer positions are **canvas** coordinates (see [`layout::CanvasLayout`]).
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
mod anchors;
|
||||
pub mod catalog;
|
||||
mod frame;
|
||||
mod gradient;
|
||||
|
|
@ -259,6 +260,9 @@ pub fn create(id: &str) -> Box<dyn Tool> {
|
|||
"type" => Box::new(text::TypeTool::default()),
|
||||
"pen" => Box::new(pen::PenTool::default()),
|
||||
"gradientSwatch" => Box::new(gradient::GradientTool::default()),
|
||||
"addAnchor" => Box::new(anchors::AnchorTool::new(anchors::Kind::Add)),
|
||||
"deleteAnchor" => Box::new(anchors::AnchorTool::new(anchors::Kind::Delete)),
|
||||
"convertDirection" => Box::new(anchors::AnchorTool::new(anchors::Kind::Convert)),
|
||||
"rotate" => Box::new(xform::XformTool::new("rotate")),
|
||||
"scale" => Box::new(xform::XformTool::new("scale")),
|
||||
"shear" => Box::new(xform::XformTool::new("shear")),
|
||||
|
|
|
|||
|
|
@ -23,8 +23,13 @@ enum Drag {
|
|||
|
||||
/// Anchor or handle of a selected item under `p` (canvas): (item, subpath, anchor, handle).
|
||||
pub fn anchor_at(cx: &ToolContext, p: Point) -> Option<(u64, usize, usize, Option<&'static str>)> {
|
||||
anchor_at_in(cx, &cx.selection.items, p)
|
||||
}
|
||||
|
||||
/// [`anchor_at`] over the given items.
|
||||
pub fn anchor_at_in(cx: &ToolContext, ids: &[designcraft_doc::ItemId], p: Point) -> Option<(u64, usize, usize, Option<&'static str>)> {
|
||||
let tol = cx.tol(5.0);
|
||||
for id in &cx.selection.items {
|
||||
for id in ids {
|
||||
let (Some(it), Some(xf)) = (cx.doc.item(*id), cx.item_canvas_xf(*id)) else { continue };
|
||||
let m = xf * it.xf;
|
||||
for (si, sp) in it.path.subpaths.iter().enumerate() {
|
||||
|
|
|
|||
|
|
@ -104,3 +104,41 @@ fn gradient_tool_drag_sets_the_vector() {
|
|||
}
|
||||
assert_eq!(t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 220.0 + off.x, 153.0 + off.y)), vec![Action::Commit]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn anchor_tools_emit_path_commands() {
|
||||
let mut d = Document::new(&NewDocument::default());
|
||||
let lid = d.default_layer();
|
||||
let (fid, _) = d.add_text_frame(SpreadRef::Doc(0), Rect::new(100.0, 100.0, 200.0, 200.0), lid, "", ParaFormat::default()).unwrap();
|
||||
let (s, c, l) = (Selection::items(vec![fid]), Cache::new(), CanvasLayout::new(&d, false));
|
||||
let cx = ctx(&d, &s, &c, &l);
|
||||
let off = l.offset(SpreadRef::Doc(0));
|
||||
let at = |x: f64, y: f64| (x + off.x, y + off.y);
|
||||
let ev = |k, (x, y): (f64, f64)| PointerEvent::new(k, x, y);
|
||||
let mut add = create("addAnchor");
|
||||
match &add.pointer(&cx, &ev(PointerKind::Down, at(150.0, 101.0)))[..] {
|
||||
[Action::Exec(cmd, p)] => {
|
||||
assert_eq!(cmd, "path.addAnchor");
|
||||
assert_eq!(p["at"], serde_json::json!([150.0, 101.0]));
|
||||
}
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
assert!(add.pointer(&cx, &ev(PointerKind::Down, at(150.0, 150.0))).is_empty(), "inside, away from the edge");
|
||||
let mut del = create("deleteAnchor");
|
||||
assert_eq!(
|
||||
del.pointer(&cx, &ev(PointerKind::Down, at(200.0, 100.0))),
|
||||
vec![Action::Exec("path.deleteAnchor".into(), serde_json::json!({"id": fid.0, "subpath": 0, "anchor": 1}))]
|
||||
);
|
||||
// Convert: click toggles, drag pulls out handles.
|
||||
let mut conv = create("convertDirection");
|
||||
conv.pointer(&cx, &ev(PointerKind::Down, at(100.0, 100.0)));
|
||||
assert_eq!(
|
||||
conv.pointer(&cx, &ev(PointerKind::Up, at(100.0, 100.0))),
|
||||
vec![Action::Exec("path.convertAnchor".into(), serde_json::json!({"id": fid.0, "subpath": 0, "anchor": 0}))]
|
||||
);
|
||||
conv.pointer(&cx, &ev(PointerKind::Down, at(100.0, 100.0)));
|
||||
let a = conv.pointer(&cx, &ev(PointerKind::Drag, at(120.0, 90.0)));
|
||||
assert_eq!(a[0], Action::Begin("Convert Direction Point".into()));
|
||||
assert!(matches!(&a[1], Action::Preview(c, p) if c == "path.convertAnchor" && p["to"] == serde_json::json!([120.0, 90.0])));
|
||||
assert_eq!(conv.pointer(&cx, &ev(PointerKind::Up, at(120.0, 90.0))), vec![Action::Commit]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -656,7 +656,7 @@ fn draw_selection(app: &DesignApp, painter: &egui::Painter, xf: &Xf, doc: &Docum
|
|||
painter.rect_stroke(gb, 0.0, Stroke::new(1.0, Color32::from_rgb(196, 111, 43)), StrokeKind::Middle);
|
||||
}
|
||||
}
|
||||
let direct = matches!(app.session.tool_id(), "directSelection" | "pen");
|
||||
let direct = matches!(app.session.tool_id(), "directSelection" | "pen" | "addAnchor" | "deleteAnchor" | "convertDirection");
|
||||
if direct {
|
||||
for id in &sel.items {
|
||||
let (Some(it), Some((a, _))) = (doc.item(*id), item_canvas_xf(doc, layout, *id)) else { continue };
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
|
|||
|
||||
| Priority | Features | Done | Partial | Missing | Parity |
|
||||
|---|---|---|---|---|---|
|
||||
| P0 | 102 | 93 | 9 | 0 | 96% |
|
||||
| P0 | 102 | 94 | 8 | 0 | 96% |
|
||||
| P1 | 97 | 33 | 23 | 41 | 46% |
|
||||
| P2 | 76 | 8 | 7 | 61 | 15% |
|
||||
|
||||
|
|
@ -18,7 +18,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
|
|||
| Application shell & workspace | 65% |
|
||||
| Documents, pages, spreads | 71% |
|
||||
| Layers | 74% |
|
||||
| Frames, shapes & paths | 74% |
|
||||
| Frames, shapes & paths | 76% |
|
||||
| Transform | 78% |
|
||||
| Fill, stroke, colour | 69% |
|
||||
| Effects & transparency | 50% |
|
||||
|
|
@ -35,7 +35,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
|
|||
| Undo, history, saving | 100% |
|
||||
| Accessibility | 20% |
|
||||
|
||||
Open P0 items: Dockable panels, Preferences, Pen, add/delete anchor, convert direction, Raster formats, Vector/page formats (PDF/AI/SVG/EPS place), Display performance, OpenType features, Export to PDF (print), Display performance.
|
||||
Open P0 items: Dockable panels, Preferences, Raster formats, Vector/page formats (PDF/AI/SVG/EPS place), Display performance, OpenType features, Export to PDF (print), Display performance.
|
||||
|
||||
## Rows
|
||||
|
||||
|
|
@ -98,7 +98,7 @@ Open P0 items: Dockable panels, Preferences, Pen, add/delete anchor, convert dir
|
|||
| Frames, shapes & paths | Rectangle/Ellipse/Polygon shapes | P0 | D |
|
||||
| Frames, shapes & paths | Polygon settings | P0 | D |
|
||||
| Frames, shapes & paths | Line tool | P0 | D |
|
||||
| Frames, shapes & paths | Pen, add/delete anchor, convert direction | P0 | P |
|
||||
| Frames, shapes & paths | Pen, add/delete anchor, convert direction | P0 | D |
|
||||
| Frames, shapes & paths | Pencil, Smooth, Erase | P1 | M |
|
||||
| Frames, shapes & paths | Scissors | P1 | M |
|
||||
| Frames, shapes & paths | Content type | P0 | D |
|
||||
|
|
|
|||
Loading…
Reference in New Issue