mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 18:10:01 -06:00
Add-ons: Path Precision Tools — Corner Tool and Slide Anchor
Path Precision Tools gains two tools, kept out of the toolbars (All Tools drawer and Object › Plug-ins › Path Precision Tools, no shortcuts): - Corner Tool (cornerTool): editable corners on any path through the existing Live Corners. Hover shows an eligible anchor, a click cuts a corner with the tool's radius and kind, a drag sizes it (Shift: whole values), a corner's marker selects it and loads its values, resizes it and removes it at 0 or on a double-click; a corner never grows past what fits; an Alt-marquee cuts every eligible anchor inside. - Corners section in the Anchor Inspector: radius, kind, selected count, Apply to Selected Anchors. - Slide Anchor Along Path (slideAnchor): a drag runs an anchor along its curve, the neighbouring segments refitted by the keep-shape kernel; Shift snaps where the tangent is a multiple of 45° from the constrain angle. - Commands addon.pathtools.cornerSet, cornerRemove and slideAnchor, one undo step each, params validated and covered by the command sweep. - Cursor stand-ins, catalog rows in every language, docs and tests.
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
//! Path Precision Tools (`addon.pathtools.*`), a built-in add-on: the Anchor Eraser Brush's stroke,
|
||||
//! the Extend Path tool's extension, the Measure & Inspect tool's readouts, the Anchor Inspector's
|
||||
//! anchor edits, Split / Add Points at Crossings and the Parametric Shape Tool's shapes ([`shape`]).
|
||||
//! anchor edits, Split / Add Points at Crossings, Slide Anchor Along Path, the Corner Tool's corners
|
||||
//! ([`corners`]) and the Parametric Shape Tool's shapes ([`shape`]).
|
||||
//! Its tools, panels and submenu (Object › Plug-ins › Path Precision Tools) are the tools crate's
|
||||
//! and the UI's.
|
||||
|
||||
mod corners;
|
||||
mod shape;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -139,8 +141,18 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
has_selection,
|
||||
crossings
|
||||
),
|
||||
cmd!(
|
||||
"addon.pathtools.slideAnchor",
|
||||
"Slide Anchor Along Path",
|
||||
[],
|
||||
None,
|
||||
"{id, subpath?: index (0), anchor: index, t: -0.98..0.98 (where to: positive, a curve parameter on the segment after the anchor; negative, back from the end of the one before)} move an anchor along its path, the segments beside it refitted so the outline stays, as one undo step; ends of open paths don't slide → {id, strayed (pt)}",
|
||||
has_doc,
|
||||
slide_anchor
|
||||
),
|
||||
];
|
||||
v.extend(shape::specs());
|
||||
v.extend(corners::specs());
|
||||
v
|
||||
}
|
||||
|
||||
@@ -288,8 +300,8 @@ fn edit_whole(s: &mut Session, label: &str, changed: Vec<(NodeId, PathData)>) ->
|
||||
|
||||
// ---------- Extend Path ----------
|
||||
|
||||
/// The open subpath an `extend` request names: (path id, subpath index, its data).
|
||||
fn open_subpath(s: &Session, p: &Value, cmd: &str) -> Result<(NodeId, usize, SubPath)> {
|
||||
/// The subpath a request names: (path id, subpath index, its data).
|
||||
fn named_subpath(s: &Session, p: &Value, cmd: &str) -> Result<(NodeId, usize, SubPath)> {
|
||||
let id = checked_id(s, p.get("id").unwrap_or(&Value::Null), cmd)?;
|
||||
let si = usize_opt(p, "subpath", cmd)?.unwrap_or(0);
|
||||
let d = &s.doc()?.doc;
|
||||
@@ -298,15 +310,33 @@ fn open_subpath(s: &Session, p: &Value, cmd: &str) -> Result<(NodeId, usize, Sub
|
||||
}
|
||||
let path = d.node(id).and_then(|n| n.path_data()).ok_or_else(|| bad(cmd, "`id` is not a path"))?;
|
||||
let sp = path.subpaths.get(si).ok_or_else(|| bad(cmd, "no such subpath"))?;
|
||||
if sp.closed || sp.anchors.len() < 2 {
|
||||
return Err(bad(cmd, "the subpath has no ends to extend (it is closed or a single point)"));
|
||||
}
|
||||
Ok((id, si, sp.clone()))
|
||||
}
|
||||
|
||||
/// Put `new` in place of the subpath `old` (from [`named_subpath`]) as one step labelled `label`
|
||||
/// (none when it is the same); the path's direct selection goes.
|
||||
fn set_subpath(s: &mut Session, label: &str, (id, si, old): (NodeId, usize, SubPath), new: SubPath) -> Result<()> {
|
||||
if new == old {
|
||||
return Ok(());
|
||||
}
|
||||
s.edit(label, |d, sel| {
|
||||
let mut np = d.node(id).and_then(|n| n.path_data()).cloned().ok_or(EngineError::NoNode(id))?;
|
||||
if let Some(slot) = np.subpaths.get_mut(si) {
|
||||
*slot = new;
|
||||
}
|
||||
sel.anchors.remove(&id);
|
||||
sel.segments.remove(&id);
|
||||
set_path(d, id, np);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn extend(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "addon.pathtools.extend";
|
||||
let (id, si, sp) = open_subpath(s, p, C)?;
|
||||
let (id, si, sp) = named_subpath(s, p, C)?;
|
||||
if sp.closed || sp.anchors.len() < 2 {
|
||||
return Err(bad(C, "the subpath has no ends to extend (it is closed or a single point)"));
|
||||
}
|
||||
let at_start = match text_param(p, "end", C)? {
|
||||
Some("start") => true,
|
||||
Some("end") => false,
|
||||
@@ -328,22 +358,26 @@ fn extend(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
return Err(bad(C, "`length` must be a number of at most 1000000 pt either way"));
|
||||
}
|
||||
let new = pt::extend_open(&sp, at_start, length, mode).ok_or_else(|| bad(C, "the path is too short to shorten that much"))?;
|
||||
if new != sp {
|
||||
s.edit("Extend Path", |d, sel| {
|
||||
let n = d.node(id).and_then(|n| n.path_data()).cloned().ok_or(EngineError::NoNode(id))?;
|
||||
let mut np = n;
|
||||
if let Some(slot) = np.subpaths.get_mut(si) {
|
||||
*slot = new;
|
||||
}
|
||||
sel.anchors.remove(&id);
|
||||
sel.segments.remove(&id);
|
||||
set_path(d, id, np);
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
set_subpath(s, "Extend Path", (id, si, sp), new)?;
|
||||
Ok(json!({ "id": id.0, "length": length }))
|
||||
}
|
||||
|
||||
// ---------- Slide Anchor Along Path ----------
|
||||
|
||||
fn slide_anchor(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "addon.pathtools.slideAnchor";
|
||||
let (id, si, sp) = named_subpath(s, p, C)?;
|
||||
let anchor = usize_opt(p, "anchor", C)?.ok_or_else(|| bad(C, "give `anchor`, the index of the anchor to slide"))?;
|
||||
let t = number_or(p, "t", f64::NAN, C)?;
|
||||
if t.is_nan() || t.abs() > pt::MAX_SLIDE {
|
||||
return Err(bad(C, "`t` must be a number from -0.98 to 0.98"));
|
||||
}
|
||||
let (new, strayed) =
|
||||
pt::slide_anchor(&sp, anchor, t).ok_or_else(|| bad(C, "that anchor can't slide: an end of an open path has nowhere to go"))?;
|
||||
set_subpath(s, "Slide Anchor Along Path", (id, si, sp), new)?;
|
||||
Ok(json!({ "id": id.0, "strayed": strayed }))
|
||||
}
|
||||
|
||||
// ---------- Measure & Inspect ----------
|
||||
|
||||
fn polar(h: Option<(f64, f64)>) -> Value {
|
||||
|
||||
Reference in New Issue
Block a user