mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-31 21:27:00 -06:00
Add-ons: Parametric Shape Tool — re-editable stars, polygons, rings, arrows, gears and more
Path Precision Tools gains the Parametric Shape Tool: ten kinds of shape (rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, speech bubble) that stay editable as numbers. - Geometry in vectorcraft_pathops::addons::pathtools::shapes: each kind's outline, corner rounding limited to the middle of a side, a fingerprint of the outline, recovery of centre, size and rotation after a move, turn or scale, and recognition of plain rectangles, ellipses and regular polygons. - The tool (parametricShape, All Tools drawer and Object › Plug-ins › Path Precision Tools only, no shortcut): drag from the centre (Shift: 45° steps), arrow keys change the count and values while dragging, handles edit a selected shape, double-click places one. - Commands addon.pathtools.shape.create / .set / .convert, one undo step each; params checked (counts 3..1000, sizes up to 16384 pt). - A shape keeps its numbers in one object-data entry (vc-pathtools-shape) that SVG export already leaves out; anchor edits make it a plain path. - Parametric Shape panel, cursor stand-in, catalog rows in all 25 languages, docs and tests.
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
//! 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 and Split / Add Points at Crossings. Its tools, panel and submenu (Object ›
|
||||
//! Plug-ins › Path Precision Tools) are the tools crate's and the UI's.
|
||||
//! anchor edits, Split / Add Points at Crossings 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 shape;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
@@ -37,7 +39,7 @@ const MAX_EXTEND: f64 = 1e6;
|
||||
const MAX_ADD: u64 = pt::MAX_ADDED_PER_SEGMENT as u64;
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
let mut v = vec![
|
||||
cmd!(
|
||||
"addon.pathtools.erase",
|
||||
"Anchor Eraser Brush",
|
||||
@@ -137,7 +139,9 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
has_selection,
|
||||
crossings
|
||||
),
|
||||
]
|
||||
];
|
||||
v.extend(shape::specs());
|
||||
v
|
||||
}
|
||||
|
||||
/// The editable paths (not guides) under `roots`, compound path members included.
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
//! The Parametric Shape Tool's commands (`addon.pathtools.shape.*`): draw a parametric shape, change
|
||||
//! one's numbers, and turn plain rectangles, ellipses and regular polygons into them. A parametric
|
||||
//! shape is an ordinary path whose numbers sit in one entry of its object data
|
||||
//! (`vectorcraft_tools::addons::parametric_shape`), which SVG export leaves out.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::{Node, NodeKind};
|
||||
use vectorcraft_geom::{FillRule, PathData, Point};
|
||||
use vectorcraft_pathops::addons::pathtools::shapes::{ParametricShape, ShapeKind, recognise};
|
||||
use vectorcraft_tools::addons::parametric_shape::{CREATE, EDIT, KEY, apply, read, stored, to_json};
|
||||
|
||||
use super::super::super::{CommandSpec, bad, checked_id, checked_ids_or_selection, cmd, has_doc, has_selection};
|
||||
use crate::{EngineError, Result, Session};
|
||||
|
||||
/// The undo step's name and the command's label.
|
||||
const CONVERT: &str = "Convert to Parametric Shape";
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
cmd!(
|
||||
"addon.pathtools.shape.create",
|
||||
"Parametric Shape",
|
||||
[],
|
||||
None,
|
||||
"{center: [x, y], kind?: \"rectangle\"|\"ellipse\"|\"polygon\" (default)|\"star\"|\"ring\"|\"arrow\"|\"cross\"|\"heart\"|\"gear\"|\"bubble\" (a speech bubble), width?, height?: 0.01..16384 pt (100), rotation?: degrees clockwise (0: upright), count?: 3..1000 (a polygon's sides, a star's points, a gear's teeth), a?, b?: the kind's own values (star: inner radius 0.01..1 of the outer, 0.5; ring: hole 0.01..0.99 of the size, 0.5; arrow: head length and shaft width 0.05..0.95, 0.4 and 0.4; cross: bar offset −0.9..0.9 (0, − up) and arm thickness 0.05..0.95 (1/3); heart: notch depth and point roundness 0..1 (0.4 and 0); gear: tooth height 0.01..0.9 (0.2); bubble: tail height 0.05..0.9 (0.25) and tail tip −1..1 across (−0.4)), radius?: corner radius 0..16384 pt (0; no corner rounds past the middle of a side)} draw a parametric shape with the current paint, selected, as one undo step; it stays editable with addon.pathtools.shape.set until its anchors are edited (moving, turning or scaling it keeps it) → {id, shape}",
|
||||
has_doc,
|
||||
create
|
||||
),
|
||||
cmd!(
|
||||
"addon.pathtools.shape.set",
|
||||
"Edit Parametric Shape",
|
||||
[],
|
||||
None,
|
||||
"{id, kind?, center?, width?, height?, rotation?, count?, a?, b?, radius?} change a parametric shape's numbers (as addon.pathtools.shape.create takes them; a new kind takes its own defaults for the values not given), as one undo step; with none, or the same ones, nothing changes → {id, shape (its numbers now)}",
|
||||
has_doc,
|
||||
set
|
||||
),
|
||||
cmd!(
|
||||
"addon.pathtools.shape.convert",
|
||||
"Convert to Parametric Shape",
|
||||
["Object", "Plug-ins", "Path Precision Tools"],
|
||||
None,
|
||||
"{ids?: [id…] (default: the selection)} make each plain closed path that is a rectangle, an ellipse or a polygon with equal sides and corners (stretched along its own axes at most) a parametric shape of the same outline, as one undo step → {ids (converted)}",
|
||||
has_selection,
|
||||
convert
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
/// Give path node `n` the outline `path` of shape `s` and keep its numbers; a live shape goes.
|
||||
fn shape_node(n: &mut Node, s: &ParametricShape, path: PathData) {
|
||||
let entry = stored(s, &path);
|
||||
if let NodeKind::Path { path: p, live, .. } = &mut n.kind {
|
||||
*p = path;
|
||||
*live = None;
|
||||
}
|
||||
n.edit_attrs(|a| match a.data.iter_mut().find(|(k, _)| k == KEY) {
|
||||
Some(e) => e.1 = entry,
|
||||
None => a.data.push((KEY.into(), entry)),
|
||||
});
|
||||
}
|
||||
|
||||
/// The outline of `s`, checked already.
|
||||
fn outline(s: &ParametricShape, cmd: &str) -> Result<PathData> {
|
||||
s.path().ok_or_else(|| bad(cmd, "the shape's numbers are out of range"))
|
||||
}
|
||||
|
||||
fn create(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "addon.pathtools.shape.create";
|
||||
if p.get("center").is_none_or(Value::is_null) {
|
||||
return Err(bad(C, "`center` must be [x, y]"));
|
||||
}
|
||||
let shape = apply(ParametricShape::new(ShapeKind::Polygon, Point::ZERO, 100.0, 100.0), p).map_err(|e| bad(C, e))?;
|
||||
let path = outline(&shape, C)?;
|
||||
let parent = s.doc()?.target_parent()?;
|
||||
let look = s.new_art_look(s.paint.fill.clone(), s.paint.stroke.clone(), s.paint.stroke_width);
|
||||
// shortcut: always on top of the insertion layer, as Draw Normal; honour Draw Behind/Inside if asked.
|
||||
let id = s.edit(CREATE, |d, sel| {
|
||||
let id = d.alloc_id();
|
||||
let mut n = Node::new(id, NodeKind::Path { path: PathData::default(), rule: FillRule::NonZero, live: None, clipping: false, guide: false });
|
||||
shape_node(&mut n, &shape, path);
|
||||
look.apply(d, &mut n);
|
||||
d.insert(parent, usize::MAX, n)?;
|
||||
sel.set([id]);
|
||||
Ok(id)
|
||||
})?;
|
||||
Ok(json!({ "id": id.0, "shape": to_json(&shape) }))
|
||||
}
|
||||
|
||||
fn set(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "addon.pathtools.shape.set";
|
||||
let id = checked_id(s, p.get("id").unwrap_or(&Value::Null), C)?;
|
||||
let d = &s.doc()?.doc;
|
||||
if !d.is_editable(id) {
|
||||
return Err(bad(C, "the path is locked or hidden"));
|
||||
}
|
||||
let old = d.node(id).and_then(read).ok_or_else(|| bad(C, "the object is not an editable parametric shape"))?;
|
||||
let shape = apply(old, p).map_err(|e| bad(C, e))?;
|
||||
if shape != old {
|
||||
let path = outline(&shape, C)?;
|
||||
s.edit(EDIT, |d, sel| {
|
||||
shape_node(d.node_mut(id).ok_or(EngineError::NoNode(id))?, &shape, path);
|
||||
sel.anchors.remove(&id);
|
||||
sel.segments.remove(&id);
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
Ok(json!({ "id": id.0, "shape": to_json(&shape) }))
|
||||
}
|
||||
|
||||
fn convert(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "addon.pathtools.shape.convert";
|
||||
let roots = checked_ids_or_selection(s, p, C)?;
|
||||
let d = &s.doc()?.doc;
|
||||
let mut found = vec![];
|
||||
for id in roots {
|
||||
let Some(n) = d.node(id).filter(|n| d.is_editable(id) && read(n).is_none()) else { continue };
|
||||
if let NodeKind::Path { path, guide: false, clipping: false, .. } = &n.kind
|
||||
&& let Some(shape) = recognise(path)
|
||||
{
|
||||
found.push((id, shape, outline(&shape, C)?));
|
||||
}
|
||||
}
|
||||
if found.is_empty() {
|
||||
return Err(bad(C, "select plain rectangles, ellipses or regular polygons to convert"));
|
||||
}
|
||||
let ids: Vec<u64> = found.iter().map(|(id, ..)| id.0).collect();
|
||||
s.edit(CONVERT, |d, sel| {
|
||||
for (id, shape, path) in found {
|
||||
shape_node(d.node_mut(id).ok_or(EngineError::NoNode(id))?, &shape, path);
|
||||
sel.anchors.remove(&id);
|
||||
sel.segments.remove(&id);
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(json!({ "ids": ids }))
|
||||
}
|
||||
@@ -273,3 +273,186 @@ fn crossings_add_anchors_or_cut_in_one_step() {
|
||||
assert_eq!(steps(&s), n + 1);
|
||||
refuses(&mut s, "addon.pathtools.crossings", &[json!({"cut": "yes"}), json!({"ids": [123456]})]);
|
||||
}
|
||||
|
||||
// ---------- Parametric Shape Tool ----------
|
||||
|
||||
/// The parametric shape object `id` is now, as the tool reads it.
|
||||
fn shape_of(s: &Session, id: NodeId) -> Option<vectorcraft_pathops::addons::pathtools::shapes::ParametricShape> {
|
||||
vectorcraft_tools::addons::parametric_shape::read(s.doc().unwrap().doc.node(id).unwrap())
|
||||
}
|
||||
|
||||
fn star(s: &mut Session) -> NodeId {
|
||||
let r = s.execute("addon.pathtools.shape.create", &json!({"kind": "star", "center": [200, 150], "width": 100, "height": 100})).unwrap();
|
||||
id_of(&r)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_draws_a_selected_parametric_shape_in_one_step() {
|
||||
let mut s = session();
|
||||
let n = steps(&s);
|
||||
let r = s.execute("addon.pathtools.shape.create", &json!({"kind": "gear", "center": [200, 150], "count": 10, "a": 0.3, "radius": 2})).unwrap();
|
||||
let id = id_of(&r);
|
||||
assert_eq!(steps(&s), n + 1, "one undo step");
|
||||
assert_eq!((r["shape"]["kind"].clone(), r["shape"]["count"].clone(), r["shape"]["width"].clone()), (json!("gear"), json!(10), json!(100.0)));
|
||||
assert_eq!(s.doc().unwrap().selection.objects, [id]);
|
||||
let g = shape_of(&s, id).unwrap();
|
||||
assert_eq!((g.count, g.a, g.radius), (10, 0.3, 2.0));
|
||||
assert!(path(&s, id).anchor_count() > 40, "40 tooth corners, rounded");
|
||||
// Every kind, with its defaults.
|
||||
for kind in ["rectangle", "ellipse", "polygon", "star", "ring", "arrow", "cross", "heart", "gear", "bubble"] {
|
||||
let id = id_of(&s.execute("addon.pathtools.shape.create", &json!({"kind": kind, "center": [50, 50]})).unwrap());
|
||||
assert_eq!(shape_of(&s, id).map(|s| s.kind.id()), Some(kind));
|
||||
}
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(steps(&s), n + 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_changes_the_numbers_in_one_step_and_nothing_without_a_change() {
|
||||
let mut s = session();
|
||||
let id = star(&mut s);
|
||||
assert_eq!(path(&s, id).anchor_count(), 10);
|
||||
let n = steps(&s);
|
||||
let r = s.execute("addon.pathtools.shape.set", &json!({"id": id.0, "count": 8, "a": 0.3, "rotation": 15})).unwrap();
|
||||
assert_eq!((r["shape"]["count"].clone(), r["shape"]["rotation"].clone()), (json!(8), json!(15.0)));
|
||||
assert_eq!(path(&s, id).anchor_count(), 16);
|
||||
assert_eq!(steps(&s), n + 1);
|
||||
// The same numbers again, or none: no step.
|
||||
s.execute("addon.pathtools.shape.set", &json!({"id": id.0, "count": 8})).unwrap();
|
||||
s.execute("addon.pathtools.shape.set", &json!({"id": id.0})).unwrap();
|
||||
assert_eq!(steps(&s), n + 1);
|
||||
// Another kind takes its own defaults.
|
||||
let r = s.execute("addon.pathtools.shape.set", &json!({"id": id.0, "kind": "arrow"})).unwrap();
|
||||
assert_eq!((r["shape"]["a"].clone(), r["shape"]["rotation"].clone()), (json!(0.4), json!(15.0)));
|
||||
assert_eq!(path(&s, id).anchor_count(), 7);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(path(&s, id).anchor_count(), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_shape_params_are_refused() {
|
||||
let mut s = session();
|
||||
let id = star(&mut s);
|
||||
let rect = id_of(&s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap());
|
||||
let c = json!([10, 10]);
|
||||
refuses(
|
||||
&mut s,
|
||||
"addon.pathtools.shape.create",
|
||||
&[
|
||||
json!({}),
|
||||
json!({"center": null}),
|
||||
json!({"center": [1]}),
|
||||
json!({"center": "here"}),
|
||||
json!({"center": [1e300, 0]}),
|
||||
json!({"center": c, "kind": "cloud"}),
|
||||
json!({"center": c, "kind": 3}),
|
||||
json!({"center": c, "count": 2}),
|
||||
json!({"center": c, "count": 1001}),
|
||||
json!({"center": c, "count": 4.5}),
|
||||
json!({"center": c, "width": 0}),
|
||||
json!({"center": c, "height": 16385}),
|
||||
json!({"center": c, "width": "40"}),
|
||||
json!({"center": c, "kind": "star", "a": 2}),
|
||||
json!({"center": c, "kind": "bubble", "b": -1.5}),
|
||||
json!({"center": c, "radius": -1}),
|
||||
json!({"center": c, "rotation": "up"}),
|
||||
],
|
||||
);
|
||||
refuses(
|
||||
&mut s,
|
||||
"addon.pathtools.shape.set",
|
||||
&[json!({}), json!({"id": rect.0, "count": 6}), json!({"id": 999_999}), json!({"id": id.0, "count": 0}), json!({"id": id.0, "width": -5})],
|
||||
);
|
||||
s.execute("object.setProps", &json!({"ids": [id.0], "locked": true})).unwrap();
|
||||
refuses(&mut s, "addon.pathtools.shape.set", &[json!({"id": id.0, "count": 6})]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_anchor_edit_makes_it_a_plain_path_and_transforms_keep_it_editable() {
|
||||
let mut s = session();
|
||||
let id = star(&mut s);
|
||||
// Moved, turned and scaled with the ordinary commands: still the shape, at its new place.
|
||||
s.execute("object.move", &json!({"dx": 20, "dy": -10})).unwrap();
|
||||
assert!(shape_of(&s, id).is_some_and(|s| s.center.distance(Point::new(220.0, 140.0)) < 1e-6));
|
||||
s.execute("object.rotate", &json!({"angle": 30, "origin": [220, 140]})).unwrap();
|
||||
let r = shape_of(&s, id).unwrap();
|
||||
assert!((r.rotation + 30.0).abs() < 1e-6, "30° counter-clockwise is −30 here: {r:?}");
|
||||
s.execute("object.scale", &json!({"sx": 200, "origin": [220, 140]})).unwrap();
|
||||
let r = shape_of(&s, id).unwrap();
|
||||
assert!((r.width - 200.0).abs() < 1e-6 && r.center.distance(Point::new(220.0, 140.0)) < 1e-6, "{r:?}");
|
||||
let n = steps(&s);
|
||||
s.execute("addon.pathtools.shape.set", &json!({"id": id.0, "count": 6})).unwrap();
|
||||
assert_eq!((path(&s, id).anchor_count(), steps(&s)), (12, n + 1));
|
||||
assert!((shape_of(&s, id).unwrap().rotation + 30.0).abs() < 1e-6, "the turn stays");
|
||||
// Stretched across its own axes (it is turned): skewed, so no longer.
|
||||
s.execute("object.scale", &json!({"sx": 150, "sy": 100, "origin": [220, 140]})).unwrap();
|
||||
assert_eq!(shape_of(&s, id), None);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert!(shape_of(&s, id).is_some(), "undo brings it back");
|
||||
// An anchor moved by hand: a plain path, which `set` refuses.
|
||||
select_anchors(&mut s, id, &[2]);
|
||||
s.execute("addon.pathtools.setAnchor", &json!({"x": 0})).unwrap();
|
||||
assert_eq!(shape_of(&s, id), None);
|
||||
refuses(&mut s, "addon.pathtools.shape.set", &[json!({"id": id.0, "count": 7})]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convert_makes_plain_shapes_parametric_in_one_step() {
|
||||
let mut s = session();
|
||||
let r = id_of(&s.execute("shape.rectangle", &json!({"x": 10, "y": 20, "width": 100, "height": 50})).unwrap());
|
||||
let p = id_of(&s.execute("shape.polygon", &json!({"cx": 200, "cy": 150, "radius": 40, "sides": 7})).unwrap());
|
||||
let st = id_of(&s.execute("shape.star", &json!({"cx": 300, "cy": 150, "radius1": 40, "radius2": 15})).unwrap());
|
||||
select(&mut s, &[r, p, st]);
|
||||
let n = steps(&s);
|
||||
let out = s.execute("addon.pathtools.shape.convert", &json!({})).unwrap();
|
||||
assert_eq!(out["ids"], json!([r.0, p.0]));
|
||||
assert_eq!(steps(&s), n + 1);
|
||||
assert_eq!(shape_of(&s, r).map(|s| s.kind.id()), Some("rectangle"));
|
||||
assert_eq!(shape_of(&s, p).map(|s| (s.kind.id(), s.count)), Some(("polygon", 7)));
|
||||
assert_eq!(shape_of(&s, st), None);
|
||||
assert_eq!(path(&s, r).bounds().map(|b| b.center()), Some(Point::new(60.0, 45.0)));
|
||||
// Nothing left to convert.
|
||||
select(&mut s, &[r, st]);
|
||||
refuses(&mut s, "addon.pathtools.shape.convert", &[json!({}), json!({"ids": [st.0]}), json!({"ids": [123456]})]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_shape_data_stays_out_of_svg_export() {
|
||||
let mut s = session();
|
||||
let id = star(&mut s);
|
||||
s.execute("object.setProps", &json!({"ids": [id.0], "data": {"pivot": "1,2"}})).unwrap();
|
||||
let key = vectorcraft_tools::addons::parametric_shape::KEY;
|
||||
let node = s.doc().unwrap().doc.node(id).unwrap().clone();
|
||||
assert!(node.extras().attrs.as_deref().is_some_and(|a| a.data.iter().any(|(k, _)| k == key)), "kept with the object");
|
||||
assert!(shape_of(&s, id).is_some(), "setting other data keeps it a shape");
|
||||
let svg = vectorcraft_svg::export(&s.doc().unwrap().doc, &Default::default());
|
||||
assert!(svg.contains("data-pivot=\"1,2\""), "the object's own data still goes out: {svg}");
|
||||
assert!(!svg.contains(key) && !svg.contains("fp"") && !svg.contains("\"fp\""), "{svg}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_tool_draws_one_step_per_drag_and_arrows_change_the_count() {
|
||||
let mut s = session();
|
||||
let view = ViewInfo { smart_guides: false, ..ViewInfo::default() };
|
||||
s.select_tool("parametricShape", view).unwrap();
|
||||
s.set_tool_option("kind", &json!("star"));
|
||||
let n = steps(&s);
|
||||
s.pointer(&PointerEvent::new(PointerKind::Down, 200.0, 150.0), view).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Drag, 200.0, 100.0), view).unwrap();
|
||||
s.tool_key(vectorcraft_tools::ToolKey::Up, vectorcraft_tools::Mods::default(), view).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Drag, 200.0, 90.0), view).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Up, 200.0, 90.0), view).unwrap();
|
||||
assert_eq!(steps(&s), n + 1);
|
||||
let id = *s.doc().unwrap().selection.objects.first().unwrap();
|
||||
let shape = shape_of(&s, id).unwrap();
|
||||
assert_eq!((shape.kind.id(), shape.count, shape.height, shape.rotation), ("star", 6, 120.0, 0.0));
|
||||
// Its size handle (lower right of its box) drags into one more step.
|
||||
let corner = shape.frame() * Point::new(shape.width / 2.0, shape.height / 2.0);
|
||||
for (k, x) in [(PointerKind::Down, corner.x), (PointerKind::Drag, corner.x + 30.0), (PointerKind::Up, corner.x + 30.0)] {
|
||||
s.pointer(&PointerEvent::new(k, x, corner.y), view).unwrap();
|
||||
}
|
||||
assert_eq!(steps(&s), n + 2);
|
||||
assert!((shape_of(&s, id).unwrap().width - 180.0).abs() < 1e-9);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(shape_of(&s, id).map(|s| s.width), Some(120.0));
|
||||
}
|
||||
|
||||
@@ -510,6 +510,20 @@ fn structured_junk() {
|
||||
("shapeBuilder.regions", json!({"ids": [2, 3], "gap": "x"})),
|
||||
("shapeBuilder.regions", json!({"gap": true})),
|
||||
("shapeBuilder.regions", json!({"gap": 12, "ids": [u64::MAX]})),
|
||||
// Path Precision Tools: the Parametric Shape Tool.
|
||||
("addon.pathtools.shape.create", json!({"center": [1e308, -1e308], "width": 1e308, "count": 1e308})),
|
||||
(
|
||||
"addon.pathtools.shape.create",
|
||||
json!({"center": [0, 0], "kind": "gear", "count": 1000, "a": 0.9, "radius": 16384, "width": 16384, "height": 0.01}),
|
||||
),
|
||||
("addon.pathtools.shape.create", json!({"center": [0, 0], "kind": "star", "count": 1000, "a": 0.01, "radius": 1e-300})),
|
||||
("addon.pathtools.shape.create", json!({"center": ["x", null], "kind": ["star"], "rotation": f64::NAN})),
|
||||
("addon.pathtools.shape.create", json!({"center": [5, 5], "kind": "bubble", "a": -1e308, "b": 1e308, "rotation": 1e308})),
|
||||
("addon.pathtools.shape.set", json!({"id": 2, "count": 1000, "kind": "gear"})),
|
||||
("addon.pathtools.shape.set", json!({"id": u64::MAX, "width": -1})),
|
||||
("addon.pathtools.shape.set", json!({"id": 2, "center": [1, 2, 3], "radius": "big"})),
|
||||
("addon.pathtools.shape.convert", json!({"ids": [0, 1, 2, 3, u64::MAX]})),
|
||||
("addon.pathtools.shape.convert", json!({"ids": "all"})),
|
||||
// Smart Pen add-on.
|
||||
("addon.pen.append", json!({"id": 2, "x": 1e308, "y": 0, "in": [1e308, -1e308]})),
|
||||
("addon.pen.append", json!({"id": 2, "subpath": u64::MAX, "x": 0, "y": 0, "segment": "curved"})),
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
//! its shape within a tolerance (shared by every add-on that takes anchors away).
|
||||
//! - [`cleanup`]: Artwork Cleanup's path repairs (collinear, duplicate and near anchors, tiny
|
||||
//! handles, nearly closed paths) and Rejoin Path Pieces.
|
||||
//! - [`pathtools`]: Path Precision Tools' anchor edits, path readouts, extending open paths and
|
||||
//! finding where paths cross.
|
||||
//! - [`pathtools`]: Path Precision Tools' anchor edits, path readouts, extending open paths,
|
||||
//! finding where paths cross and the Parametric Shape Tool's re-editable shapes.
|
||||
//! - [`segment_reshape`]: bending a segment by dragging a point on it (shared by the tools that
|
||||
//! reshape segments directly).
|
||||
//! - [`pen`]: the Smart Pen's straight and curved segments and its point edits.
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
//! Path Precision Tools' geometry: anchor edits for the Anchor Inspector (smooth by the auto-smooth
|
||||
//! rule, equalise handles, add evenly spaced anchors, close), the readouts the Measure & Inspect
|
||||
//! tool shows over a path, open paths lengthened or shortened at an end ([`extend`]) and the places
|
||||
//! where paths cross ([`crossings`]).
|
||||
//! where paths cross ([`crossings`]), and the Parametric Shape Tool's shapes ([`shapes`]).
|
||||
|
||||
mod crossings;
|
||||
mod extend;
|
||||
pub mod shapes;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
|
||||
@@ -0,0 +1,664 @@
|
||||
//! The Parametric Shape Tool's geometry (Path Precision Tools): shapes described by a few numbers
|
||||
//! (kind, centre, size, rotation, a count and up to two values of the kind's own, corner radius)
|
||||
//! that stay editable as those numbers. [`ParametricShape::path`] draws one; [`fingerprint`] and
|
||||
//! [`ParametricShape::recover`] tell whether a path is still the shape it was drawn as, moved,
|
||||
//! turned or scaled since, or was edited into something else; [`recognise`] finds the shape a
|
||||
//! plain rectangle, ellipse or regular polygon is.
|
||||
|
||||
use std::f64::consts::{PI, TAU};
|
||||
|
||||
use kurbo::{Affine, Point, Vec2};
|
||||
use vectorcraft_geom::corners::cut_corners;
|
||||
use vectorcraft_geom::shapes::KAPPA;
|
||||
use vectorcraft_geom::{Anchor, AnchorKind, PathData, SubPath};
|
||||
|
||||
/// The fewest sides, points or teeth.
|
||||
pub const MIN_COUNT: u32 = 3;
|
||||
/// The most sides, points or teeth.
|
||||
pub const MAX_COUNT: u32 = 1000;
|
||||
/// The smallest width or height (pt).
|
||||
pub const MIN_SIZE: f64 = 0.01;
|
||||
/// The largest width, height or corner radius (pt).
|
||||
pub const MAX_SIZE: f64 = 16384.0;
|
||||
/// The farthest from the origin a centre may be (pt).
|
||||
pub const MAX_CENTER: f64 = 1e7;
|
||||
|
||||
/// The kinds of parametric shape.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum ShapeKind {
|
||||
Rectangle,
|
||||
Ellipse,
|
||||
Polygon,
|
||||
Star,
|
||||
Ring,
|
||||
Arrow,
|
||||
Cross,
|
||||
Heart,
|
||||
Gear,
|
||||
Bubble,
|
||||
}
|
||||
|
||||
/// One of a kind's own values: its range and default.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct ValueRange {
|
||||
pub min: f64,
|
||||
pub max: f64,
|
||||
pub default: f64,
|
||||
}
|
||||
|
||||
const fn range(min: f64, max: f64, default: f64) -> Option<ValueRange> {
|
||||
Some(ValueRange { min, max, default })
|
||||
}
|
||||
|
||||
impl ShapeKind {
|
||||
pub const ALL: [ShapeKind; 10] = [
|
||||
ShapeKind::Rectangle,
|
||||
ShapeKind::Ellipse,
|
||||
ShapeKind::Polygon,
|
||||
ShapeKind::Star,
|
||||
ShapeKind::Ring,
|
||||
ShapeKind::Arrow,
|
||||
ShapeKind::Cross,
|
||||
ShapeKind::Heart,
|
||||
ShapeKind::Gear,
|
||||
ShapeKind::Bubble,
|
||||
];
|
||||
|
||||
/// The kind's name in commands and files.
|
||||
pub fn id(self) -> &'static str {
|
||||
match self {
|
||||
ShapeKind::Rectangle => "rectangle",
|
||||
ShapeKind::Ellipse => "ellipse",
|
||||
ShapeKind::Polygon => "polygon",
|
||||
ShapeKind::Star => "star",
|
||||
ShapeKind::Ring => "ring",
|
||||
ShapeKind::Arrow => "arrow",
|
||||
ShapeKind::Cross => "cross",
|
||||
ShapeKind::Heart => "heart",
|
||||
ShapeKind::Gear => "gear",
|
||||
ShapeKind::Bubble => "bubble",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(s: &str) -> Option<Self> {
|
||||
Self::ALL.into_iter().find(|k| k.id() == s)
|
||||
}
|
||||
|
||||
/// Does `count` set how many sides, points or teeth the kind has?
|
||||
pub fn counted(self) -> bool {
|
||||
matches!(self, ShapeKind::Polygon | ShapeKind::Star | ShapeKind::Gear)
|
||||
}
|
||||
|
||||
/// The count a shape of this kind starts with.
|
||||
pub fn default_count(self) -> u32 {
|
||||
match self {
|
||||
ShapeKind::Star => 5,
|
||||
ShapeKind::Gear => 12,
|
||||
_ => 6,
|
||||
}
|
||||
}
|
||||
|
||||
/// Can its corners be rounded (`radius`)? Not a shape without corners.
|
||||
pub fn rounds(self) -> bool {
|
||||
!matches!(self, ShapeKind::Ellipse | ShapeKind::Ring)
|
||||
}
|
||||
|
||||
/// The kind's own values `a` and `b` (as fractions of its size), where it has them: a star's
|
||||
/// and a ring's inner radius; an arrow's head length and shaft width; a cross's bar offset
|
||||
/// (−: up) and arm thickness; a heart's notch depth and the roundness of its point; a gear's
|
||||
/// tooth height; a speech bubble's tail height and where its tip is across (−1 left, 1 right).
|
||||
pub fn values(self) -> [Option<ValueRange>; 2] {
|
||||
match self {
|
||||
ShapeKind::Star => [range(0.01, 1.0, 0.5), None],
|
||||
ShapeKind::Ring => [range(0.01, 0.99, 0.5), None],
|
||||
ShapeKind::Arrow => [range(0.05, 0.95, 0.4), range(0.05, 0.95, 0.4)],
|
||||
ShapeKind::Cross => [range(-0.9, 0.9, 0.0), range(0.05, 0.95, 1.0 / 3.0)],
|
||||
ShapeKind::Heart => [range(0.0, 1.0, 0.4), range(0.0, 1.0, 0.0)],
|
||||
ShapeKind::Gear => [range(0.01, 0.9, 0.2), None],
|
||||
ShapeKind::Bubble => [range(0.05, 0.9, 0.25), range(-1.0, 1.0, -0.4)],
|
||||
ShapeKind::Rectangle | ShapeKind::Ellipse | ShapeKind::Polygon => [None, None],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A parametric shape: what it is and where; its outline is [`ParametricShape::path`].
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct ParametricShape {
|
||||
pub kind: ShapeKind,
|
||||
pub center: Point,
|
||||
pub width: f64,
|
||||
pub height: f64,
|
||||
/// Degrees clockwise on the page; 0 stands the shape upright (a polygon's first corner up).
|
||||
pub rotation: f64,
|
||||
/// Sides, points or teeth ([`ShapeKind::counted`]).
|
||||
pub count: u32,
|
||||
/// The kind's own values ([`ShapeKind::values`]); a kind without one ignores it.
|
||||
pub a: f64,
|
||||
pub b: f64,
|
||||
/// The corners' radius (pt), each no larger than fits ([`ShapeKind::rounds`]).
|
||||
pub radius: f64,
|
||||
}
|
||||
|
||||
/// Where a shape is wrong, as `(param, min, max)`: `param` must be from `min` to `max`.
|
||||
pub type OutOfRange = (&'static str, f64, f64);
|
||||
|
||||
impl ParametricShape {
|
||||
/// A `kind` of shape about `center`, `width` × `height`, with the kind's defaults.
|
||||
pub fn new(kind: ShapeKind, center: Point, width: f64, height: f64) -> Self {
|
||||
let [a, b] = kind.values().map(|v| v.map_or(0.0, |v| v.default));
|
||||
Self { kind, center, width, height, rotation: 0.0, count: kind.default_count(), a, b, radius: 0.0 }
|
||||
}
|
||||
|
||||
/// The shape as another kind, keeping its place, size, rotation and corner radius; its count
|
||||
/// and values are the new kind's defaults.
|
||||
pub fn with_kind(self, kind: ShapeKind) -> Self {
|
||||
if kind == self.kind {
|
||||
return self;
|
||||
}
|
||||
Self { radius: self.radius, rotation: self.rotation, ..Self::new(kind, self.center, self.width, self.height) }
|
||||
}
|
||||
|
||||
/// The first number out of its range, if any (every number must also be finite).
|
||||
pub fn check(&self) -> Result<(), OutOfRange> {
|
||||
let within = |v: f64, lo: f64, hi: f64| v.is_finite() && (lo..=hi).contains(&v);
|
||||
let size = [("width", self.width), ("height", self.height)];
|
||||
if let Some((key, _)) = size.into_iter().find(|(_, v)| !within(*v, MIN_SIZE, MAX_SIZE)) {
|
||||
return Err((key, MIN_SIZE, MAX_SIZE));
|
||||
}
|
||||
if !within(self.radius, 0.0, MAX_SIZE) {
|
||||
return Err(("radius", 0.0, MAX_SIZE));
|
||||
}
|
||||
if !(within(self.center.x, -MAX_CENTER, MAX_CENTER) && within(self.center.y, -MAX_CENTER, MAX_CENTER)) {
|
||||
return Err(("center", -MAX_CENTER, MAX_CENTER));
|
||||
}
|
||||
if !self.rotation.is_finite() {
|
||||
return Err(("rotation", -360.0, 360.0));
|
||||
}
|
||||
if !(MIN_COUNT..=MAX_COUNT).contains(&self.count) {
|
||||
return Err(("count", f64::from(MIN_COUNT), f64::from(MAX_COUNT)));
|
||||
}
|
||||
// A value the kind doesn't have is left alone.
|
||||
for (key, v, spec) in [("a", self.a, self.kind.values()[0]), ("b", self.b, self.kind.values()[1])] {
|
||||
if let Some(r) = spec.filter(|r| !within(v, r.min, r.max)) {
|
||||
return Err((key, r.min, r.max));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// From the shape's own frame (its centre at the origin, upright) to the page.
|
||||
pub fn frame(&self) -> Affine {
|
||||
Affine::translate(self.center.to_vec2()) * Affine::rotate(self.rotation.to_radians())
|
||||
}
|
||||
|
||||
/// The shape's outline on the page: closed subpaths, starting at the top (a rectangle's and a
|
||||
/// speech bubble's at the top left corner) and running clockwise; a ring's hole runs the
|
||||
/// other way. `None` when a number is out of range ([`Self::check`]).
|
||||
pub fn path(&self) -> Option<PathData> {
|
||||
self.check().ok()?;
|
||||
let (rx, ry) = (self.width / 2.0, self.height / 2.0);
|
||||
let at = |x: f64, y: f64| Point::new(x * rx, y * ry);
|
||||
let polygon = |pts: Vec<Point>| vec![SubPath::polyline(&pts, true)];
|
||||
let (a, b) = (self.a, self.b);
|
||||
let around = |n: usize, turn: f64, radius_at: &dyn Fn(usize) -> f64| -> Vec<Point> {
|
||||
(0..n)
|
||||
.map(|i| {
|
||||
let (s, c) = (i as f64 * turn).sin_cos();
|
||||
let r = radius_at(i);
|
||||
at(r * s, -r * c)
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
let n = self.count as usize;
|
||||
let subpaths = match self.kind {
|
||||
ShapeKind::Rectangle => polygon(vec![at(-1.0, -1.0), at(1.0, -1.0), at(1.0, 1.0), at(-1.0, 1.0)]),
|
||||
ShapeKind::Ellipse => vec![ellipse(rx, ry)],
|
||||
ShapeKind::Polygon => polygon(around(n, TAU / n as f64, &|_| 1.0)),
|
||||
ShapeKind::Star => polygon(around(2 * n, PI / n as f64, &|i| if i % 2 == 0 { 1.0 } else { a })),
|
||||
ShapeKind::Ring => {
|
||||
let mut hole = ellipse(rx * a, ry * a);
|
||||
hole.reverse();
|
||||
vec![ellipse(rx, ry), hole]
|
||||
}
|
||||
ShapeKind::Arrow => {
|
||||
// The head from the top down to `yb`, the shaft below it.
|
||||
let (yb, sw) = (-1.0 + 2.0 * a, b);
|
||||
polygon(vec![at(0.0, -1.0), at(1.0, yb), at(sw, yb), at(sw, 1.0), at(-sw, 1.0), at(-sw, yb), at(-1.0, yb)])
|
||||
}
|
||||
ShapeKind::Cross => {
|
||||
// The bar across, `a` of the way from the middle to an end of the upright.
|
||||
let (t, y0) = (b, a * (1.0 - b));
|
||||
let (top, bottom) = (y0 - t, y0 + t);
|
||||
polygon(vec![
|
||||
at(-t, -1.0),
|
||||
at(t, -1.0),
|
||||
at(t, top),
|
||||
at(1.0, top),
|
||||
at(1.0, bottom),
|
||||
at(t, bottom),
|
||||
at(t, 1.0),
|
||||
at(-t, 1.0),
|
||||
at(-t, bottom),
|
||||
at(-1.0, bottom),
|
||||
at(-1.0, top),
|
||||
at(-t, top),
|
||||
])
|
||||
}
|
||||
ShapeKind::Heart => vec![heart(a, b, &at)],
|
||||
ShapeKind::Gear => {
|
||||
// Each tooth's flanks and tip, centred on its angle; straight roots between them.
|
||||
let step = TAU / n as f64;
|
||||
let root = 1.0 - a;
|
||||
let mut pts = Vec::with_capacity(4 * n);
|
||||
for i in 0..n {
|
||||
for (f, r) in [(-0.3, root), (-0.15, 1.0), (0.15, 1.0), (0.3, root)] {
|
||||
let (s, c) = ((i as f64 + f) * step).sin_cos();
|
||||
pts.push(at(r * s, -r * c));
|
||||
}
|
||||
}
|
||||
polygon(pts)
|
||||
}
|
||||
ShapeKind::Bubble => {
|
||||
// The body above `by`, the tail's base on its bottom edge, its tip on the bottom.
|
||||
let by = 1.0 - 2.0 * a;
|
||||
let (mid, half) = (b * 0.5, 0.3);
|
||||
polygon(vec![at(-1.0, -1.0), at(1.0, -1.0), at(1.0, by), at(mid + half, by), at(b, 1.0), at(mid - half, by), at(-1.0, by)])
|
||||
}
|
||||
};
|
||||
let mut path = PathData::new(subpaths);
|
||||
if self.radius > 0.0 && self.kind.rounds() {
|
||||
path = cut_corners(&path, &vec![self.radius; path.anchor_count()], &[]).0;
|
||||
}
|
||||
path.transform(self.frame());
|
||||
Some(path)
|
||||
}
|
||||
|
||||
/// The shape `path` is when it is this shape's outline moved, turned or scaled (along the
|
||||
/// shape's own axes; uniformly when its corners are rounded), not mirrored or skewed, and
|
||||
/// otherwise unedited: the new centre, size, rotation and corner radius. `None` when its
|
||||
/// anchors were edited, or it was skewed or mirrored.
|
||||
pub fn recover(&self, path: &PathData) -> Option<Self> {
|
||||
let drawn = self.path()?;
|
||||
let pts = |p: &PathData| p.anchors().map(|(_, _, a)| a.p).collect::<Vec<_>>();
|
||||
let (from, to) = (pts(&drawn), pts(path));
|
||||
if from.len() != to.len() || drawn.subpaths.len() != path.subpaths.len() {
|
||||
return None;
|
||||
}
|
||||
let m = fit_affine(&from, &to)? * self.frame();
|
||||
let [m0, m1, m2, m3, ..] = m.as_coeffs();
|
||||
let (x, y) = (Vec2::new(m0, m1), Vec2::new(m2, m3));
|
||||
let (sx, sy) = (x.hypot(), y.hypot());
|
||||
// Square and not mirrored: the shape's axes stay at right angles, turned the same way.
|
||||
if !(sx > 1e-12 && sy > 1e-12) || x.dot(y).abs() > 1e-6 * sx * sy || x.cross(y) <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
let out = Self {
|
||||
center: m * Point::ZERO,
|
||||
width: self.width * sx,
|
||||
height: self.height * sy,
|
||||
rotation: vectorcraft_geom::normalize_deg(x.y.atan2(x.x).to_degrees()),
|
||||
radius: self.radius * (sx * sy).sqrt(),
|
||||
..*self
|
||||
};
|
||||
out.check().ok()?;
|
||||
same(&out.path()?, path).then_some(out)
|
||||
}
|
||||
}
|
||||
|
||||
/// An ellipse of radii `rx`, `ry` about the origin: four smooth anchors from the top, clockwise.
|
||||
fn ellipse(rx: f64, ry: f64) -> SubPath {
|
||||
let (kx, ky) = (rx * KAPPA, ry * KAPPA);
|
||||
let s = |p: Point, out: Vec2| Anchor { p, h_in: p - out, h_out: p + out, kind: AnchorKind::Smooth };
|
||||
SubPath::new(
|
||||
vec![
|
||||
s(Point::new(0.0, -ry), Vec2::new(kx, 0.0)),
|
||||
s(Point::new(rx, 0.0), Vec2::new(0.0, ky)),
|
||||
s(Point::new(0.0, ry), Vec2::new(-kx, 0.0)),
|
||||
s(Point::new(-rx, 0.0), Vec2::new(0.0, -ky)),
|
||||
],
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
/// A heart in the unit box (`at` scales it): two lobes over a point at the bottom, the notch
|
||||
/// between the lobes `depth` of the way down to the middle, the point rounder with `round`.
|
||||
fn heart(depth: f64, round: f64, at: &dyn Fn(f64, f64) -> Point) -> SubPath {
|
||||
let ny = -1.0 + depth;
|
||||
let notch = at(0.0, ny);
|
||||
let mirror = |x: f64, y: f64| [at(x, y), at(-x, y)];
|
||||
let [notch_out, notch_in] = mirror(0.1, ny - 0.3 * (1.0 + ny));
|
||||
let [tip_in, tip_out] = mirror(0.2 + 0.4 * round, 0.7 + 0.3 * round);
|
||||
let a = |p, h_in, h_out, kind| Anchor { p, h_in, h_out, kind };
|
||||
let smooth = AnchorKind::Smooth;
|
||||
SubPath::new(
|
||||
vec![
|
||||
a(notch, notch_in, notch_out, AnchorKind::Corner),
|
||||
a(at(0.5, -1.0), at(0.2, -1.0), at(0.8, -1.0), smooth),
|
||||
a(at(1.0, -0.35), at(1.0, -0.75), at(1.0, 0.1), smooth),
|
||||
a(at(0.0, 1.0), tip_in, tip_out, AnchorKind::Corner),
|
||||
a(at(-1.0, -0.35), at(-1.0, 0.1), at(-1.0, -0.75), smooth),
|
||||
a(at(-0.5, -1.0), at(-0.8, -1.0), at(-0.2, -1.0), smooth),
|
||||
],
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
/// The affine map taking each of `from` to the point of `to` at its index, worked out from
|
||||
/// three of them far apart; `None` when they all lie on a line.
|
||||
fn fit_affine(from: &[Point], to: &[Point]) -> Option<Affine> {
|
||||
let (&p0, &q0) = (from.first()?, to.first()?);
|
||||
let far = (0..from.len()).max_by(|&i, &j| dist(from, i, p0).total_cmp(&dist(from, j, p0)))?;
|
||||
let p1 = *from.get(far)?;
|
||||
let wide = |i: usize| from.get(i).map_or(0.0, |p| (p1 - p0).cross(*p - p0).abs());
|
||||
let third = (0..from.len()).max_by(|&i, &j| wide(i).total_cmp(&wide(j)))?;
|
||||
let (p2, q1, q2) = (*from.get(third)?, *to.get(far)?, *to.get(third)?);
|
||||
let basis = |o: Point, u: Vec2, v: Vec2| Affine::new([u.x, u.y, v.x, v.y, o.x, o.y]);
|
||||
let g = basis(p0, p1 - p0, p2 - p0);
|
||||
let det = g.determinant();
|
||||
// Too thin a triangle to tell a turn from a skew.
|
||||
if det.is_nan() || det.abs() <= 1e-9 * (p1 - p0).hypot2().max(1e-12) {
|
||||
return None;
|
||||
}
|
||||
Some(basis(q0, q1 - q0, q2 - q0) * g.inverse())
|
||||
}
|
||||
|
||||
fn dist(pts: &[Point], i: usize, from: Point) -> f64 {
|
||||
pts.get(i).map_or(0.0, |p| p.distance(from))
|
||||
}
|
||||
|
||||
/// Are `a` and `b` the same outline, anchor for anchor and handle for handle (to well under a
|
||||
/// thousandth of a point at the size of the art)?
|
||||
fn same(a: &PathData, b: &PathData) -> bool {
|
||||
let scale = a.anchors().map(|(_, _, x)| x.p.x.abs().max(x.p.y.abs())).fold(1.0, f64::max);
|
||||
let tol = 1e-3_f64.max(scale * 1e-9);
|
||||
a.subpaths.len() == b.subpaths.len()
|
||||
&& a.subpaths.iter().zip(&b.subpaths).all(|(s, t)| {
|
||||
s.closed == t.closed
|
||||
&& s.anchors.len() == t.anchors.len()
|
||||
&& s.anchors
|
||||
.iter()
|
||||
.zip(&t.anchors)
|
||||
.all(|(x, y)| x.p.distance(y.p) <= tol && x.h_in.distance(y.h_in) <= tol && x.h_out.distance(y.h_out) <= tol)
|
||||
})
|
||||
}
|
||||
|
||||
/// A fingerprint of `path`'s geometry: its subpaths and every anchor and handle to a thousandth of
|
||||
/// a point. Kept with a shape, it shows whether the path is still the one drawn.
|
||||
pub fn fingerprint(path: &PathData) -> u64 {
|
||||
// FNV-1a: the same in every build, as a fingerprint saved in a file must be.
|
||||
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
|
||||
let mut eat = |v: i64| {
|
||||
for byte in v.to_le_bytes() {
|
||||
h = (h ^ u64::from(byte)).wrapping_mul(0x0100_0000_01b3);
|
||||
}
|
||||
};
|
||||
for sp in &path.subpaths {
|
||||
eat(sp.anchors.len() as i64);
|
||||
eat(i64::from(sp.closed));
|
||||
for a in &sp.anchors {
|
||||
for p in [a.p, a.h_in, a.h_out] {
|
||||
// Saturating casts: a huge or non-finite coordinate still hashes.
|
||||
eat((p.x * 1000.0).round() as i64);
|
||||
eat((p.y * 1000.0).round() as i64);
|
||||
}
|
||||
}
|
||||
}
|
||||
h
|
||||
}
|
||||
|
||||
/// The parametric shape a plain closed path is, if it is one: a rectangle (four corners, no
|
||||
/// handles), an ellipse (four smooth anchors) or a polygon whose sides and corners are all equal,
|
||||
/// stretched along its own axes at most. The shape's outline starts at its own first anchor, so
|
||||
/// the path's anchors may come in a different order.
|
||||
pub fn recognise(path: &PathData) -> Option<ParametricShape> {
|
||||
let [sp] = path.subpaths.as_slice() else { return None };
|
||||
let n = sp.anchors.len();
|
||||
if !sp.closed || !(3..=MAX_COUNT as usize).contains(&n) {
|
||||
return None;
|
||||
}
|
||||
let corners = sp.anchors.iter().all(|a| !a.has_in() && !a.has_out());
|
||||
let kinds: &[ShapeKind] = match (n, corners) {
|
||||
(4, true) => &[ShapeKind::Rectangle, ShapeKind::Polygon],
|
||||
(4, false) => &[ShapeKind::Ellipse],
|
||||
(_, true) => &[ShapeKind::Polygon],
|
||||
_ => return None,
|
||||
};
|
||||
let unit = |kind| ParametricShape { count: n as u32, ..ParametricShape::new(kind, Point::ZERO, 2.0, 2.0) };
|
||||
// The path read from each anchor in turn, either way round.
|
||||
for reversed in [false, true] {
|
||||
let mut sp = sp.clone();
|
||||
if reversed {
|
||||
sp.reverse();
|
||||
}
|
||||
for _ in 0..n {
|
||||
let candidate = PathData::single(sp.clone());
|
||||
if let Some(s) = kinds.iter().find_map(|k| unit(*k).recover(&candidate)) {
|
||||
return Some(s);
|
||||
}
|
||||
sp.anchors.rotate_left(1);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn shape(kind: ShapeKind) -> ParametricShape {
|
||||
ParametricShape::new(kind, Point::new(100.0, 50.0), 80.0, 60.0)
|
||||
}
|
||||
|
||||
fn closed(p: &PathData) -> bool {
|
||||
p.subpaths.iter().all(|s| s.closed)
|
||||
}
|
||||
|
||||
/// Every point of `p` lies in the shape's box (a little slack for curves).
|
||||
fn within_box(p: &PathData, s: &ParametricShape) -> bool {
|
||||
let b = p.bounds().unwrap();
|
||||
let (hw, hh) = (s.width / 2.0 + 1e-6, s.height / 2.0 + 1e-6);
|
||||
b.x0 >= s.center.x - hw && b.x1 <= s.center.x + hw && b.y0 >= s.center.y - hh && b.y1 <= s.center.y + hh
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_kind_draws_a_closed_outline_in_its_box() {
|
||||
for kind in ShapeKind::ALL {
|
||||
let s = shape(kind);
|
||||
assert_eq!(ShapeKind::parse(kind.id()), Some(kind));
|
||||
assert_eq!(s.check(), Ok(()), "{kind:?}");
|
||||
let p = s.path().unwrap();
|
||||
assert!(closed(&p) && within_box(&p, &s), "{kind:?}: {:?}", p.bounds());
|
||||
assert!(p.anchors().all(|(_, _, a)| a.p.x.is_finite() && a.p.y.is_finite()));
|
||||
}
|
||||
assert_eq!(ShapeKind::parse("cloud"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn anchor_counts_and_corners_per_kind() {
|
||||
let count = |kind, n: u32| ParametricShape { count: n, ..shape(kind) }.path().unwrap().anchor_count();
|
||||
assert_eq!(count(ShapeKind::Rectangle, 3), 4);
|
||||
assert_eq!(count(ShapeKind::Ellipse, 3), 4);
|
||||
assert_eq!(count(ShapeKind::Polygon, 7), 7);
|
||||
assert_eq!(count(ShapeKind::Polygon, 1000), 1000);
|
||||
assert_eq!(count(ShapeKind::Star, 5), 10);
|
||||
assert_eq!(count(ShapeKind::Gear, 12), 48);
|
||||
assert_eq!(count(ShapeKind::Arrow, 3), 7);
|
||||
assert_eq!(count(ShapeKind::Cross, 3), 12);
|
||||
assert_eq!(count(ShapeKind::Heart, 3), 6);
|
||||
assert_eq!(count(ShapeKind::Bubble, 3), 7);
|
||||
let ring = shape(ShapeKind::Ring).path().unwrap();
|
||||
assert_eq!(ring.subpaths.len(), 2);
|
||||
// The hole runs the other way, so it stays a hole under the non-zero rule.
|
||||
assert!(ring.subpaths[0].signed_area() * ring.subpaths[1].signed_area() < 0.0);
|
||||
let (outer, inner) = (ring.subpaths[0].area().abs(), ring.subpaths[1].area().abs());
|
||||
assert!((inner / outer - 0.25).abs() < 1e-3, "inner ratio 0.5 → a quarter of the area: {}", inner / outer);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn polygons_and_stars_stand_upright_and_turn_clockwise() {
|
||||
let p = shape(ShapeKind::Polygon).path().unwrap();
|
||||
let top = p.subpaths[0].anchors[0].p;
|
||||
assert!(top.distance(Point::new(100.0, 20.0)) < 1e-9, "first corner straight up: {top:?}");
|
||||
let turned = ParametricShape { rotation: 90.0, ..shape(ShapeKind::Polygon) }.path().unwrap();
|
||||
let right = turned.subpaths[0].anchors[0].p;
|
||||
assert!(right.distance(Point::new(130.0, 50.0)) < 1e-9, "a quarter turn clockwise puts it on the right: {right:?}");
|
||||
let star = shape(ShapeKind::Star).path().unwrap();
|
||||
let inner = star.subpaths[0].anchors[1].p.distance(Point::new(100.0, 50.0));
|
||||
let outer = star.subpaths[0].anchors[0].p.distance(Point::new(100.0, 50.0));
|
||||
assert!((outer - 30.0).abs() < 1e-9 && inner < outer * 0.6, "{outer} {inner}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn values_change_each_kind() {
|
||||
let differ = |kind: ShapeKind, a: f64, b: f64| {
|
||||
let s = shape(kind);
|
||||
let t = ParametricShape { a, b, ..s };
|
||||
assert_eq!(t.check(), Ok(()), "{kind:?}");
|
||||
assert_ne!(s.path(), t.path(), "{kind:?} a {a} b {b}");
|
||||
};
|
||||
differ(ShapeKind::Star, 0.2, 0.0);
|
||||
differ(ShapeKind::Ring, 0.8, 0.0);
|
||||
differ(ShapeKind::Arrow, 0.6, 0.4);
|
||||
differ(ShapeKind::Arrow, 0.4, 0.2);
|
||||
differ(ShapeKind::Cross, 0.5, 1.0 / 3.0);
|
||||
differ(ShapeKind::Cross, 0.0, 0.6);
|
||||
differ(ShapeKind::Heart, 0.8, 0.0);
|
||||
differ(ShapeKind::Heart, 0.4, 1.0);
|
||||
differ(ShapeKind::Gear, 0.5, 0.0);
|
||||
differ(ShapeKind::Bubble, 0.5, -0.4);
|
||||
differ(ShapeKind::Bubble, 0.25, 0.8);
|
||||
// An arrow's head reaches down `a` of the height.
|
||||
let arrow = ParametricShape { a: 0.5, ..shape(ShapeKind::Arrow) }.path().unwrap();
|
||||
assert!((arrow.subpaths[0].anchors[1].p.y - 50.0).abs() < 1e-9);
|
||||
// A gear's roots sit `a` of the radius in.
|
||||
let gear = shape(ShapeKind::Gear).path().unwrap();
|
||||
let root = gear.subpaths[0].anchors[0].p - Point::new(100.0, 50.0);
|
||||
assert!((root.x / 40.0).hypot(root.y / 30.0) - 0.8 < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corners_round_but_never_past_the_middle_of_a_side() {
|
||||
let sharp = shape(ShapeKind::Rectangle);
|
||||
let round = ParametricShape { radius: 10.0, ..sharp };
|
||||
let p = round.path().unwrap();
|
||||
assert_eq!(p.anchor_count(), 8);
|
||||
assert_eq!(p.bounds(), sharp.path().unwrap().bounds());
|
||||
// Far too big a radius: the corners meet in the middle of the short sides.
|
||||
let huge = ParametricShape { radius: 1000.0, ..sharp }.path().unwrap();
|
||||
let b = huge.bounds().unwrap();
|
||||
assert!((b.width() - 80.0).abs() < 1e-6 && (b.height() - 60.0).abs() < 1e-6);
|
||||
// Kinds without corners ignore it.
|
||||
let e = ParametricShape { radius: 10.0, ..shape(ShapeKind::Ellipse) };
|
||||
assert_eq!(e.path(), shape(ShapeKind::Ellipse).path());
|
||||
for kind in ShapeKind::ALL.into_iter().filter(|k| k.rounds()) {
|
||||
let p = ParametricShape { radius: 3.0, ..shape(kind) }.path().unwrap();
|
||||
assert!(p.anchor_count() > shape(kind).path().unwrap().anchor_count(), "{kind:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn numbers_out_of_range_are_refused() {
|
||||
let s = shape(ShapeKind::Star);
|
||||
let bad = [
|
||||
(ParametricShape { width: 0.0, ..s }, "width"),
|
||||
(ParametricShape { height: MAX_SIZE * 2.0, ..s }, "height"),
|
||||
(ParametricShape { width: f64::NAN, ..s }, "width"),
|
||||
(ParametricShape { count: 2, ..s }, "count"),
|
||||
(ParametricShape { count: MAX_COUNT + 1, ..s }, "count"),
|
||||
(ParametricShape { a: 1.5, ..s }, "a"),
|
||||
(ParametricShape { a: f64::NAN, ..s }, "a"),
|
||||
(ParametricShape { radius: -1.0, ..s }, "radius"),
|
||||
(ParametricShape { rotation: f64::INFINITY, ..s }, "rotation"),
|
||||
(ParametricShape { center: Point::new(1e300, 0.0), ..s }, "center"),
|
||||
];
|
||||
for (shape, key) in bad {
|
||||
assert_eq!(shape.check().map_err(|e| e.0), Err(key));
|
||||
assert_eq!(shape.path(), None);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_kind_keeps_the_place_and_takes_the_new_kinds_defaults() {
|
||||
let s = ParametricShape { rotation: 30.0, radius: 4.0, count: 9, ..shape(ShapeKind::Polygon) };
|
||||
let star = s.with_kind(ShapeKind::Star);
|
||||
assert_eq!((star.center, star.width, star.rotation, star.radius, star.count, star.a), (s.center, 80.0, 30.0, 4.0, 5, 0.5));
|
||||
let arrow = star.with_kind(ShapeKind::Arrow);
|
||||
assert_eq!((arrow.count, arrow.a, arrow.b), (6, 0.4, 0.4));
|
||||
assert_eq!(arrow.check(), Ok(()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_moved_turned_or_scaled_shape_is_recovered() {
|
||||
for kind in ShapeKind::ALL {
|
||||
let s = ParametricShape { rotation: 20.0, ..shape(kind) };
|
||||
let p = s.path().unwrap();
|
||||
assert_eq!(s.recover(&p).map(|r| r.kind), Some(kind), "unchanged {kind:?}");
|
||||
let xf = Affine::translate((40.0, -15.0)) * Affine::rotate_about(0.5, s.center);
|
||||
let r = s.recover(&p.transformed(xf)).unwrap_or_else(|| panic!("{kind:?} moved and turned"));
|
||||
assert!((r.rotation - (20.0 + 0.5f64.to_degrees())).abs() < 1e-6 && (r.width - 80.0).abs() < 1e-6, "{r:?}");
|
||||
assert!(r.center.distance(Point::new(140.0, 35.0)) < 1e-6);
|
||||
// Scaled along its own axes.
|
||||
let along = s.frame() * Affine::scale_non_uniform(1.5, 0.5) * s.frame().inverse();
|
||||
let r = s.recover(&p.transformed(along)).unwrap_or_else(|| panic!("{kind:?} scaled"));
|
||||
assert!((r.width - 120.0).abs() < 1e-6 && (r.height - 30.0).abs() < 1e-6, "{kind:?} {r:?}");
|
||||
}
|
||||
// Rounded corners scale uniformly only.
|
||||
let s = ParametricShape { radius: 5.0, ..shape(ShapeKind::Star) };
|
||||
let p = s.path().unwrap();
|
||||
let r = s.recover(&p.transformed(Affine::scale_about(2.0, s.center))).unwrap();
|
||||
assert!((r.radius - 10.0).abs() < 1e-9 && (r.width - 160.0).abs() < 1e-9);
|
||||
assert_eq!(s.recover(&p.transformed(Affine::scale_non_uniform(2.0, 1.0))), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edited_skewed_or_mirrored_shapes_are_not() {
|
||||
let s = shape(ShapeKind::Star);
|
||||
let p = s.path().unwrap();
|
||||
let mut edited = p.clone();
|
||||
edited.anchor_mut(0, 3).unwrap().translate(Vec2::new(5.0, 0.0));
|
||||
assert_eq!(s.recover(&edited), None);
|
||||
assert_eq!(s.recover(&p.transformed(Affine::skew(0.3, 0.0))), None);
|
||||
assert_eq!(s.recover(&p.transformed(Affine::FLIP_X)), None);
|
||||
let mut more = p.clone();
|
||||
more.subpaths[0].insert_anchor(0, 0.5);
|
||||
assert_eq!(s.recover(&more), None);
|
||||
assert_eq!(s.recover(&PathData::default()), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_fingerprint_follows_the_geometry() {
|
||||
let p = shape(ShapeKind::Gear).path().unwrap();
|
||||
assert_eq!(fingerprint(&p), fingerprint(&p.clone()));
|
||||
assert_eq!(fingerprint(&p), fingerprint(&p.transformed(Affine::translate((1e-5, 0.0)))), "below a thousandth of a point");
|
||||
assert_ne!(fingerprint(&p), fingerprint(&p.transformed(Affine::translate((0.01, 0.0)))));
|
||||
let mut edited = p.clone();
|
||||
edited.anchor_mut(0, 5).unwrap().h_out.x += 1.0;
|
||||
assert_ne!(fingerprint(&p), fingerprint(&edited));
|
||||
let mut weird = p.clone();
|
||||
weird.anchor_mut(0, 0).unwrap().p.x = f64::NAN;
|
||||
let _ = fingerprint(&weird);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plain_rectangles_ellipses_and_regular_polygons_are_recognised() {
|
||||
use vectorcraft_geom::shapes;
|
||||
let r = recognise(&shapes::rectangle(kurbo::Rect::new(10.0, 20.0, 110.0, 70.0))).unwrap();
|
||||
assert_eq!(r.kind, ShapeKind::Rectangle);
|
||||
assert!(r.center.distance(Point::new(60.0, 45.0)) < 1e-9);
|
||||
let (w, h) = if r.rotation.abs() < 1e-6 || (r.rotation.abs() - 180.0).abs() < 1e-6 { (r.width, r.height) } else { (r.height, r.width) };
|
||||
assert!((w - 100.0).abs() < 1e-9 && (h - 50.0).abs() < 1e-9, "{r:?}");
|
||||
let e = recognise(&shapes::ellipse(kurbo::Rect::new(0.0, 0.0, 40.0, 20.0))).unwrap();
|
||||
assert_eq!(e.kind, ShapeKind::Ellipse);
|
||||
assert!(e.center.distance(Point::new(20.0, 10.0)) < 1e-9);
|
||||
let poly = recognise(&shapes::polygon(Point::new(50.0, 50.0), 30.0, 7, 10.0)).unwrap();
|
||||
assert_eq!((poly.kind, poly.count), (ShapeKind::Polygon, 7));
|
||||
assert!((poly.width - 60.0).abs() < 1e-6);
|
||||
// Its outline is the polygon's, if from another first anchor.
|
||||
let (a, b) = (poly.path().unwrap().bounds().unwrap(), shapes::polygon(Point::new(50.0, 50.0), 30.0, 7, 10.0).bounds().unwrap());
|
||||
assert!(a.center().distance(b.center()) < 1e-9 && (a.area() - b.area()).abs() < 1e-6, "{a:?} {b:?}");
|
||||
// Not regular, open, or a star: no.
|
||||
assert_eq!(recognise(&PathData::single(SubPath::polyline(&[Point::ZERO, Point::new(10.0, 0.0), Point::new(3.0, 8.0)], true))), None);
|
||||
assert_eq!(recognise(&PathData::single(SubPath::polyline(&[Point::ZERO, Point::new(10.0, 0.0), Point::new(10.0, 10.0)], false))), None);
|
||||
assert_eq!(recognise(&shapes::star(Point::ZERO, 10.0, 4.0, 5, 0.0)), None);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
pub mod anchor_eraser;
|
||||
pub mod extend_path;
|
||||
pub mod measure_inspect;
|
||||
pub mod parametric_shape;
|
||||
pub mod smart_pen;
|
||||
|
||||
use crate::{Tool, ToolInfo};
|
||||
@@ -14,6 +15,7 @@ pub const PATH_TOOLS: &[ToolInfo] = &[
|
||||
ToolInfo { id: anchor_eraser::ID, label: "Anchor Eraser Brush", shortcut: None, icon: "dc-remove-brush" },
|
||||
ToolInfo { id: extend_path::ID, label: "Extend Path Tool", shortcut: None, icon: "dc-arrow-extend" },
|
||||
ToolInfo { id: measure_inspect::ID, label: "Measure & Inspect Tool", shortcut: None, icon: "ruler" },
|
||||
ToolInfo { id: parametric_shape::ID, label: "Parametric Shape Tool", shortcut: None, icon: "hexagon" },
|
||||
];
|
||||
|
||||
/// Smart Pen's tool, a flyout group of its own.
|
||||
@@ -37,6 +39,7 @@ pub fn create(id: &str) -> Option<Box<dyn Tool>> {
|
||||
anchor_eraser::ID => Box::new(anchor_eraser::AnchorEraserTool::default()),
|
||||
extend_path::ID => Box::new(extend_path::ExtendPathTool::default()),
|
||||
measure_inspect::ID => Box::new(measure_inspect::MeasureInspectTool::default()),
|
||||
parametric_shape::ID => Box::new(parametric_shape::ParametricShapeTool::default()),
|
||||
smart_pen::ID => Box::new(smart_pen::SmartPenTool::default()),
|
||||
_ => return None,
|
||||
})
|
||||
|
||||
@@ -0,0 +1,620 @@
|
||||
//! Parametric Shape Tool (Path Precision Tools): shapes that stay editable as numbers. A drag from
|
||||
//! the centre draws one (`addon.pathtools.shape.create`): the drag's length is its size and its
|
||||
//! direction its rotation (Shift: 45° steps); ↑/↓ change the kind's count or first value while
|
||||
//! dragging, ←/→ its second value or corner radius. A click selects a parametric shape and shows
|
||||
//! its handles (centre, size, rotation, corner radius and one of the kind's own), whose drags run
|
||||
//! `addon.pathtools.shape.set`; a double-click on the canvas places one with the last settings.
|
||||
//! Each gesture is one undo step.
|
||||
//!
|
||||
//! A parametric shape is an ordinary path that keeps its numbers in one entry of its object data
|
||||
//! ([`KEY`]) with a fingerprint of the outline they drew. Moved, turned or scaled by any tool it
|
||||
//! stays editable ([`ParametricShape::recover`]); once its anchors are edited it is a plain path.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::hit::hit_test;
|
||||
use vectorcraft_doc::{Node, NodeId, NodeKind};
|
||||
use vectorcraft_geom::{Affine, BezPath, PathData, Point, Vec2};
|
||||
use vectorcraft_pathops::addons::pathtools::shapes::fingerprint;
|
||||
pub use vectorcraft_pathops::addons::pathtools::shapes::{MAX_COUNT, MAX_SIZE, MIN_COUNT, MIN_SIZE, ParametricShape, ShapeKind};
|
||||
|
||||
use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey};
|
||||
|
||||
/// The tool's id.
|
||||
pub const ID: &str = "parametricShape";
|
||||
/// The object data entry that holds a parametric shape's numbers (JSON). The `vc-` prefix marks it
|
||||
/// as VectorCraft's own, which SVG export leaves out of the `data-*` attributes it writes.
|
||||
pub const KEY: &str = "vc-pathtools-shape";
|
||||
/// The longest entry read back (a damaged file's longer one is no shape).
|
||||
const MAX_STORED: usize = 4096;
|
||||
/// The undo steps' names.
|
||||
pub const CREATE: &str = "Parametric Shape";
|
||||
pub const EDIT: &str = "Edit Parametric Shape";
|
||||
const BLUE: [u8; 3] = [0x4f, 0x9d, 0xff];
|
||||
/// How far (px) the rotation handle stands above the shape.
|
||||
const STEM_PX: f64 = 24.0;
|
||||
|
||||
/// `s` as commands take it and files keep it.
|
||||
pub fn to_json(s: &ParametricShape) -> Value {
|
||||
json!({
|
||||
"kind": s.kind.id(), "center": [s.center.x, s.center.y], "width": s.width, "height": s.height,
|
||||
"rotation": s.rotation, "count": s.count, "a": s.a, "b": s.b, "radius": s.radius,
|
||||
})
|
||||
}
|
||||
|
||||
/// `s` with the numbers `p` gives (as [`to_json`] writes them; any left out stay as they are). A
|
||||
/// new `kind` comes first, with its own defaults for what `p` leaves out. Every number is checked:
|
||||
/// the error says which is wrong.
|
||||
pub fn apply(mut s: ParametricShape, p: &Value) -> Result<ParametricShape, String> {
|
||||
let given = |key: &str| p.get(key).filter(|v| !v.is_null());
|
||||
let num = |key: &str| -> Result<Option<f64>, String> {
|
||||
given(key).map(|v| v.as_f64().filter(|x| x.is_finite()).ok_or_else(|| format!("`{key}` must be a number"))).transpose()
|
||||
};
|
||||
if let Some(k) = given("kind") {
|
||||
let kind = k.as_str().and_then(ShapeKind::parse);
|
||||
s = s.with_kind(kind.ok_or("`kind` must be one of: rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble")?);
|
||||
}
|
||||
if let Some(c) = given("center") {
|
||||
let xy = c.as_array().filter(|a| a.len() == 2).and_then(|a| Some((a.first()?.as_f64()?, a.get(1)?.as_f64()?)));
|
||||
let (x, y) = xy.ok_or("`center` must be [x, y]")?;
|
||||
s.center = Point::new(x, y);
|
||||
}
|
||||
for (key, slot) in [("width", &mut s.width), ("height", &mut s.height), ("a", &mut s.a), ("b", &mut s.b), ("radius", &mut s.radius)] {
|
||||
if let Some(v) = num(key)? {
|
||||
*slot = v;
|
||||
}
|
||||
}
|
||||
if let Some(r) = num("rotation")? {
|
||||
s.rotation = vectorcraft_geom::normalize_deg(r);
|
||||
}
|
||||
if let Some(n) = num("count")? {
|
||||
if n.fract() != 0.0 || !(f64::from(MIN_COUNT)..=f64::from(MAX_COUNT)).contains(&n) {
|
||||
return Err("`count` must be a whole number from 3 to 1000".into());
|
||||
}
|
||||
s.count = n as u32;
|
||||
}
|
||||
s.check().map_err(|(key, lo, hi)| format!("`{key}` must be from {lo} to {hi}"))?;
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
/// What a parametric shape keeps in its object data ([`KEY`]): its numbers and the fingerprint of
|
||||
/// `path`, the outline they drew.
|
||||
pub fn stored(s: &ParametricShape, path: &PathData) -> String {
|
||||
let mut v = to_json(s);
|
||||
v["fp"] = json!(format!("{:016x}", fingerprint(path)));
|
||||
v.to_string()
|
||||
}
|
||||
|
||||
/// The parametric shape object `n` is, if it is one: a path (not a guide) with a [`KEY`] entry
|
||||
/// whose fingerprint is its outline's, or which it is moved, turned or scaled. `None` for any
|
||||
/// other object, a damaged entry, or a path whose anchors were edited since.
|
||||
pub fn read(n: &Node) -> Option<ParametricShape> {
|
||||
let NodeKind::Path { path, guide: false, .. } = &n.kind else { return None };
|
||||
let (_, text) = n.extras().attrs.as_deref()?.data.iter().find(|(k, _)| k == KEY)?;
|
||||
if text.len() > MAX_STORED {
|
||||
return None;
|
||||
}
|
||||
let v: Value = serde_json::from_str(text).ok()?;
|
||||
let s = apply(ParametricShape::new(ShapeKind::Polygon, Point::ZERO, 100.0, 100.0), &v).ok()?;
|
||||
let fp = v.get("fp").and_then(Value::as_str).and_then(|f| u64::from_str_radix(f, 16).ok())?;
|
||||
if fp == fingerprint(path) { Some(s) } else { s.recover(path) }
|
||||
}
|
||||
|
||||
/// The values the arrow keys change while drawing: ↑/↓ the first, ←/→ the second.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum Field {
|
||||
Count,
|
||||
A,
|
||||
B,
|
||||
Radius,
|
||||
}
|
||||
|
||||
/// The kind's (↑/↓, ←/→) values.
|
||||
fn arrow_fields(kind: ShapeKind) -> (Option<Field>, Option<Field>) {
|
||||
match kind {
|
||||
ShapeKind::Rectangle => (None, Some(Field::Radius)),
|
||||
ShapeKind::Ellipse => (None, None),
|
||||
ShapeKind::Polygon => (Some(Field::Count), Some(Field::Radius)),
|
||||
ShapeKind::Star | ShapeKind::Gear => (Some(Field::Count), Some(Field::A)),
|
||||
ShapeKind::Ring => (Some(Field::A), None),
|
||||
ShapeKind::Arrow | ShapeKind::Cross | ShapeKind::Heart | ShapeKind::Bubble => (Some(Field::A), Some(Field::B)),
|
||||
}
|
||||
}
|
||||
|
||||
/// `s` with `field` one step up (`up`) or down: a count by one, a value by 5 % of its range, the
|
||||
/// corner radius by `step` pt; each kept within its range.
|
||||
fn stepped(mut s: ParametricShape, field: Field, up: bool, step: f64) -> ParametricShape {
|
||||
let sign = if up { 1.0 } else { -1.0 };
|
||||
let value = |v: f64, slot: usize| match s.kind.values().get(slot).copied().flatten() {
|
||||
Some(r) => (v + sign * 0.05 * (r.max - r.min)).clamp(r.min, r.max),
|
||||
None => v,
|
||||
};
|
||||
match field {
|
||||
Field::Count => s.count = if up { (s.count + 1).min(MAX_COUNT) } else { s.count.saturating_sub(1).max(MIN_COUNT) },
|
||||
Field::A => s.a = value(s.a, 0),
|
||||
Field::B => s.b = value(s.b, 1),
|
||||
Field::Radius => s.radius = (s.radius + sign * step).clamp(0.0, MAX_SIZE),
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
/// The handles of a selected shape.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum Grip {
|
||||
/// The centre: moves the shape.
|
||||
Move,
|
||||
/// The box's lower right corner: sizes it about its centre (Shift keeps its proportions).
|
||||
Size,
|
||||
/// Above the top: turns it (Shift: 45° steps).
|
||||
Rotate,
|
||||
/// On the box's top edge, the radius in from its left end: the corner radius.
|
||||
Corner,
|
||||
/// One of the kind's own values, where it shows.
|
||||
Value,
|
||||
}
|
||||
|
||||
/// Where the kind's own value handle is, in the shape's unit box (−1..1 each way, y down).
|
||||
fn value_spot(s: &ParametricShape) -> Option<Point> {
|
||||
let n = f64::from(s.count.max(1));
|
||||
Some(match s.kind {
|
||||
ShapeKind::Star => {
|
||||
let (sn, cs) = (std::f64::consts::PI / n).sin_cos();
|
||||
Point::new(s.a * sn, -s.a * cs)
|
||||
}
|
||||
ShapeKind::Ring => Point::new(0.0, -s.a),
|
||||
ShapeKind::Gear => Point::new(0.0, -(1.0 - s.a)),
|
||||
ShapeKind::Arrow => Point::new(1.0, -1.0 + 2.0 * s.a),
|
||||
ShapeKind::Cross => Point::new(-1.0, s.a * (1.0 - s.b)),
|
||||
ShapeKind::Heart => Point::new(0.0, -1.0 + s.a),
|
||||
ShapeKind::Bubble => Point::new(s.b, 1.0),
|
||||
ShapeKind::Rectangle | ShapeKind::Ellipse | ShapeKind::Polygon => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// `s` with its own value set from the handle dragged to `u` (unit box), within its range.
|
||||
fn value_from(mut s: ParametricShape, u: Point) -> ParametricShape {
|
||||
let clamp = |v: f64, slot: usize| s.kind.values().get(slot).copied().flatten().map_or(v, |r| v.clamp(r.min, r.max));
|
||||
match s.kind {
|
||||
ShapeKind::Star | ShapeKind::Ring => s.a = clamp(u.to_vec2().hypot(), 0),
|
||||
ShapeKind::Gear => s.a = clamp(1.0 - u.to_vec2().hypot(), 0),
|
||||
ShapeKind::Arrow => s.a = clamp((u.y + 1.0) / 2.0, 0),
|
||||
ShapeKind::Cross => s.a = clamp(u.y / (1.0 - s.b).max(1e-9), 0),
|
||||
ShapeKind::Heart => s.a = clamp(u.y + 1.0, 0),
|
||||
ShapeKind::Bubble => s.b = clamp(u.x, 1),
|
||||
ShapeKind::Rectangle | ShapeKind::Ellipse | ShapeKind::Polygon => {}
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
/// The handles of `s` and where they are on the page.
|
||||
fn grips(cx: &ToolContext, s: &ParametricShape) -> Vec<(Grip, Point)> {
|
||||
let (f, rx, ry) = (s.frame(), s.width / 2.0, s.height / 2.0);
|
||||
let mut out = vec![(Grip::Move, s.center), (Grip::Size, f * Point::new(rx, ry)), (Grip::Rotate, f * Point::new(0.0, -ry - cx.tol(STEM_PX)))];
|
||||
if s.kind.rounds() {
|
||||
out.push((Grip::Corner, f * Point::new(-rx + s.radius.min(rx), -ry)));
|
||||
}
|
||||
if let Some(u) = value_spot(s) {
|
||||
out.push((Grip::Value, f * Point::new(u.x * rx, u.y * ry)));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The angle (degrees clockwise from straight up) of `p` seen from `c`; with Shift in 45° steps.
|
||||
fn angle(c: Point, p: Point, shift: bool) -> f64 {
|
||||
let d = p - c;
|
||||
let deg = d.x.atan2(-d.y).to_degrees();
|
||||
vectorcraft_geom::normalize_deg(if shift { (deg / 45.0).round() * 45.0 } else { deg })
|
||||
}
|
||||
|
||||
/// A drag under way.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
enum Drag {
|
||||
/// Drawing a shape out from its centre.
|
||||
Create { center: Point },
|
||||
/// Dragging a handle of shape `id`, which was `shape` when the drag began at `from`.
|
||||
Edit { id: NodeId, shape: ParametricShape, grip: Grip, from: Point },
|
||||
}
|
||||
|
||||
pub struct ParametricShapeTool {
|
||||
/// What the next shape is (its kind and numbers, and the size and rotation a double-click
|
||||
/// gives it: the last drawn one's).
|
||||
next: ParametricShape,
|
||||
drag: Option<Drag>,
|
||||
/// Where the pointer last was during the drag, and the keys held.
|
||||
last: (Point, Mods),
|
||||
}
|
||||
|
||||
impl Default for ParametricShapeTool {
|
||||
fn default() -> Self {
|
||||
Self { next: ParametricShape::new(ShapeKind::Polygon, Point::ZERO, 100.0, 100.0), drag: None, last: (Point::ZERO, Mods::default()) }
|
||||
}
|
||||
}
|
||||
|
||||
/// The one selected object when it is an editable parametric shape.
|
||||
fn selected(cx: &ToolContext) -> Option<(NodeId, ParametricShape)> {
|
||||
let [id] = cx.selection.objects.as_slice() else { return None };
|
||||
let s = read(cx.doc.node(*id)?)?;
|
||||
cx.doc.is_editable(*id).then_some((*id, s))
|
||||
}
|
||||
|
||||
/// The editable parametric shape under `p`.
|
||||
fn shape_at(cx: &ToolContext, p: Point) -> Option<NodeId> {
|
||||
let id = hit_test(cx.doc, p, cx.hit_options())?.leaf;
|
||||
(cx.doc.is_editable(id) && read(cx.doc.node(id)?).is_some()).then_some(id)
|
||||
}
|
||||
|
||||
impl ParametricShapeTool {
|
||||
/// The shape a drag to `p` with `mods` makes, and the command that makes it.
|
||||
fn shaped(&self, drag: &Drag, p: Point, mods: Mods) -> (&'static str, ParametricShape, Option<NodeId>) {
|
||||
let fit = |v: f64| v.clamp(MIN_SIZE, MAX_SIZE);
|
||||
match *drag {
|
||||
Drag::Create { center } => {
|
||||
let r = center.distance(p);
|
||||
let aspect = self.next.width / self.next.height;
|
||||
let s = ParametricShape {
|
||||
center,
|
||||
width: fit(2.0 * r * aspect),
|
||||
height: fit(2.0 * r),
|
||||
rotation: angle(center, p, mods.shift),
|
||||
..self.next
|
||||
};
|
||||
("addon.pathtools.shape.create", s, None)
|
||||
}
|
||||
Drag::Edit { id, shape: s, grip, from } => {
|
||||
let local = s.frame().inverse() * p;
|
||||
let (rx, ry) = (s.width / 2.0, s.height / 2.0);
|
||||
let out = match grip {
|
||||
Grip::Move => ParametricShape { center: s.center + (p - from), ..s },
|
||||
Grip::Size if mods.shift => {
|
||||
let k = (local.x.abs() / rx).max(local.y.abs() / ry);
|
||||
ParametricShape { width: fit(s.width * k), height: fit(s.height * k), ..s }
|
||||
}
|
||||
Grip::Size => ParametricShape { width: fit(2.0 * local.x.abs()), height: fit(2.0 * local.y.abs()), ..s },
|
||||
Grip::Rotate => ParametricShape { rotation: angle(s.center, p, mods.shift), ..s },
|
||||
Grip::Corner => ParametricShape { radius: (local.x + rx).clamp(0.0, rx.min(MAX_SIZE)), ..s },
|
||||
Grip::Value => value_from(s, Point::new(local.x / rx, local.y / ry)),
|
||||
};
|
||||
("addon.pathtools.shape.set", out, Some(id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The preview of the drag under way at the pointer's last place.
|
||||
fn preview(&self) -> Option<Action> {
|
||||
let drag = self.drag.as_ref()?;
|
||||
let (cmd, s, id) = self.shaped(drag, self.last.0, self.last.1);
|
||||
let mut p = to_json(&s);
|
||||
if let Some(id) = id {
|
||||
p["id"] = json!(id.0);
|
||||
}
|
||||
Some(Action::Preview(cmd.into(), p))
|
||||
}
|
||||
}
|
||||
|
||||
impl Tool for ParametricShapeTool {
|
||||
fn id(&self) -> &'static str {
|
||||
ID
|
||||
}
|
||||
|
||||
fn busy(&self) -> bool {
|
||||
self.drag.is_some()
|
||||
}
|
||||
|
||||
fn pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec<Action> {
|
||||
match ev.kind {
|
||||
PointerKind::Move => vec![],
|
||||
PointerKind::Down => {
|
||||
let tol = cx.point_tol();
|
||||
let grip = selected(cx).and_then(|(id, s)| {
|
||||
let near = grips(cx, &s).into_iter().filter(|(_, at)| at.distance(ev.pos) <= tol);
|
||||
near.min_by(|a, b| a.1.distance(ev.pos).total_cmp(&b.1.distance(ev.pos))).map(|(g, _)| (id, s, g))
|
||||
});
|
||||
if let Some((id, shape, grip)) = grip {
|
||||
self.drag = Some(Drag::Edit { id, shape, grip, from: ev.pos });
|
||||
self.last = (ev.pos, ev.mods);
|
||||
return vec![Action::Begin(EDIT.into())];
|
||||
}
|
||||
if let Some(id) = shape_at(cx, ev.pos) {
|
||||
return if cx.selection.objects == [id] { vec![] } else { vec![Action::Exec("select.set".into(), json!({ "ids": [id.0] }))] };
|
||||
}
|
||||
let (center, _) = crate::guides::snap_draw(cx, ev.pos, &[]);
|
||||
self.drag = Some(Drag::Create { center });
|
||||
self.last = (center, ev.mods);
|
||||
vec![Action::Begin(CREATE.into())]
|
||||
}
|
||||
PointerKind::Drag | PointerKind::Up => {
|
||||
if self.drag.is_none() {
|
||||
return vec![];
|
||||
}
|
||||
self.last = (ev.pos, ev.mods);
|
||||
let Some(preview) = self.preview() else { return vec![] };
|
||||
if ev.kind == PointerKind::Drag {
|
||||
return vec![preview];
|
||||
}
|
||||
let Some(drag) = self.drag.take() else { return vec![] };
|
||||
match drag {
|
||||
// A click draws nothing (a double-click places a shape).
|
||||
Drag::Create { center } if center.distance(ev.pos) <= cx.tol(2.0) => vec![Action::Cancel],
|
||||
Drag::Create { .. } => {
|
||||
if let Action::Preview(_, p) = &preview
|
||||
&& let Ok(s) = apply(self.next, p)
|
||||
{
|
||||
self.next = ParametricShape { center: Point::ZERO, ..s };
|
||||
}
|
||||
vec![preview, Action::Commit]
|
||||
}
|
||||
Drag::Edit { from, .. } if from == ev.pos => vec![Action::Cancel],
|
||||
Drag::Edit { .. } => vec![preview, Action::Commit],
|
||||
}
|
||||
}
|
||||
PointerKind::DoubleClick => {
|
||||
if self.drag.is_some() || shape_at(cx, ev.pos).is_some() {
|
||||
return vec![];
|
||||
}
|
||||
let s = ParametricShape { center: ev.pos, ..self.next };
|
||||
vec![Action::Exec("addon.pathtools.shape.create".into(), to_json(&s))]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn key(&mut self, cx: &ToolContext, key: ToolKey, _mods: Mods) -> Vec<Action> {
|
||||
if key == ToolKey::Escape && self.drag.take().is_some() {
|
||||
return vec![Action::Cancel];
|
||||
}
|
||||
if !matches!(self.drag, Some(Drag::Create { .. })) {
|
||||
return vec![];
|
||||
}
|
||||
let (primary, secondary) = arrow_fields(self.next.kind);
|
||||
let (field, up) = match key {
|
||||
ToolKey::Up => (primary, true),
|
||||
ToolKey::Down => (primary, false),
|
||||
ToolKey::Right => (secondary, true),
|
||||
ToolKey::Left => (secondary, false),
|
||||
_ => return vec![],
|
||||
};
|
||||
let Some(field) = field else { return vec![] };
|
||||
self.next = stepped(self.next, field, up, cx.keyboard_increment.max(0.01));
|
||||
self.preview().into_iter().collect()
|
||||
}
|
||||
|
||||
fn claims_key(&self, _cx: &ToolContext, key: ToolKey) -> bool {
|
||||
match self.drag {
|
||||
Some(Drag::Create { .. }) => matches!(key, ToolKey::Up | ToolKey::Down | ToolKey::Left | ToolKey::Right | ToolKey::Escape),
|
||||
Some(Drag::Edit { .. }) => key == ToolKey::Escape,
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn overlays(&self, cx: &ToolContext) -> Vec<Overlay> {
|
||||
if let Some(Drag::Create { center }) = &self.drag {
|
||||
let (_, s, _) = self.shaped(&Drag::Create { center: *center }, self.last.0, self.last.1);
|
||||
let text = format!("{}\nAngle: {:.1}°", cx.size_label(s.width, s.height), s.rotation);
|
||||
return vec![Overlay::Measure { p: self.last.0 + Vec2::new(cx.tol(12.0), cx.tol(12.0)), text }];
|
||||
}
|
||||
let Some((_, s)) = selected(cx) else { return vec![] };
|
||||
// The shape's box, then its handles.
|
||||
let (rx, ry) = (s.width / 2.0, s.height / 2.0);
|
||||
let mut bp = BezPath::new();
|
||||
let f: Affine = s.frame();
|
||||
let corners = [(-rx, -ry), (rx, -ry), (rx, ry), (-rx, ry)].map(|(x, y)| f * Point::new(x, y));
|
||||
for (i, p) in corners.iter().enumerate() {
|
||||
if i == 0 { bp.move_to(*p) } else { bp.line_to(*p) }
|
||||
}
|
||||
bp.close_path();
|
||||
let mut out = vec![Overlay::Path { path: bp, color: BLUE, width: 1.0, dashed: true }];
|
||||
for (grip, p) in grips(cx, &s) {
|
||||
if grip == Grip::Rotate {
|
||||
out.push(Overlay::Line { a: f * Point::new(0.0, -ry), b: p, color: BLUE, dashed: false });
|
||||
}
|
||||
out.push(match grip {
|
||||
Grip::Move | Grip::Size => Overlay::Anchor { p, color: BLUE, filled: grip == Grip::Move, size: 7.0 },
|
||||
Grip::Rotate => Overlay::Handle { p, color: BLUE },
|
||||
Grip::Corner | Grip::Value => Overlay::CurvePoint { p, color: BLUE, filled: grip == Grip::Value, size: 7.0 },
|
||||
});
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn cursor(&self, _cx: &ToolContext, _p: Point, _mods: Mods) -> Cursor {
|
||||
Cursor::ParametricShape
|
||||
}
|
||||
|
||||
/// The next shape's numbers (its size and rotation are a double-click's).
|
||||
fn options(&self) -> Value {
|
||||
let mut v = to_json(&self.next);
|
||||
if let Some(o) = v.as_object_mut() {
|
||||
o.remove("center");
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
fn set_option(&mut self, key: &str, v: &Value) {
|
||||
if key != "center"
|
||||
&& let Ok(s) = apply(self.next, &json!({ key: v }))
|
||||
{
|
||||
self.next = s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::testutil::*;
|
||||
use vectorcraft_doc::{Appearance, Document, Selection};
|
||||
|
||||
fn ev(kind: PointerKind, x: f64, y: f64) -> PointerEvent {
|
||||
PointerEvent::new(kind, x, y)
|
||||
}
|
||||
|
||||
/// A document with a parametric star about (250, 250), 100 × 100: the document and its id.
|
||||
fn doc_with_star() -> (Document, NodeId) {
|
||||
let mut d = Document::new(500.0, 500.0);
|
||||
let l = d.layers[0].id;
|
||||
let id = d.alloc_id();
|
||||
let s = ParametricShape::new(ShapeKind::Star, Point::new(250.0, 250.0), 100.0, 100.0);
|
||||
let path = s.path().unwrap();
|
||||
let mut n = Node::path(id, path.clone(), Appearance::default_art());
|
||||
n.edit_attrs(|a| a.data.push((KEY.into(), stored(&s, &path))));
|
||||
d.insert(Some(l), 0, n).unwrap();
|
||||
(d, id)
|
||||
}
|
||||
|
||||
fn preview_of(acts: &[Action]) -> (String, Value) {
|
||||
match acts.first() {
|
||||
Some(Action::Preview(c, p)) => (c.clone(), p.clone()),
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_drag_from_the_centre_draws_sized_and_turned_and_arrows_change_its_numbers() {
|
||||
let (d, _) = doc_with_rect();
|
||||
let (sel, p) = (Selection::default(), paint());
|
||||
let cx = cx(&d, &sel, &p);
|
||||
let mut t = ParametricShapeTool::default();
|
||||
assert_eq!(t.pointer(&cx, &ev(PointerKind::Down, 300.0, 300.0)), vec![Action::Begin(CREATE.into())]);
|
||||
// 40 pt to the right: 80 across, turned a quarter clockwise.
|
||||
let acts = t.pointer(&cx, &ev(PointerKind::Drag, 340.0, 300.0));
|
||||
let (cmd, p) = preview_of(&acts);
|
||||
assert_eq!(cmd, "addon.pathtools.shape.create");
|
||||
assert_eq!(
|
||||
(p["kind"].clone(), p["center"].clone(), p["width"].clone(), p["rotation"].clone()),
|
||||
(json!("polygon"), json!([300.0, 300.0]), json!(80.0), json!(90.0))
|
||||
);
|
||||
assert!(t.claims_key(&cx, ToolKey::Up) && t.busy());
|
||||
// ↑ adds a side, ← takes a point off the corner radius (none left: stays 0), → adds one.
|
||||
let (_, p) = preview_of(&t.key(&cx, ToolKey::Up, Mods::default()));
|
||||
assert_eq!(p["count"], json!(7));
|
||||
let (_, p) = preview_of(&t.key(&cx, ToolKey::Right, Mods::default()));
|
||||
assert_eq!(p["radius"], json!(1.0));
|
||||
// Shift turns in 45° steps.
|
||||
let shifted = ev(PointerKind::Drag, 340.0, 290.0).with_mods(Mods { shift: true, ..Mods::default() });
|
||||
let (_, p) = preview_of(&t.pointer(&cx, &shifted));
|
||||
assert_eq!(p["rotation"], json!(90.0));
|
||||
let up = t.pointer(&cx, &ev(PointerKind::Up, 300.0, 250.0));
|
||||
assert!(
|
||||
matches!(up.as_slice(), [Action::Preview(_, p), Action::Commit] if p["rotation"] == json!(0.0) && p["height"] == json!(100.0)),
|
||||
"{up:?}"
|
||||
);
|
||||
assert!(!t.busy() && !t.claims_key(&cx, ToolKey::Up), "arrows nudge again");
|
||||
// The next shape remembers the numbers; a double-click places it.
|
||||
assert_eq!((t.options()["count"].clone(), t.options()["radius"].clone()), (json!(7), json!(1.0)));
|
||||
let acts = t.pointer(&cx, &ev(PointerKind::DoubleClick, 50.0, 400.0));
|
||||
let [Action::Exec(cmd, p)] = acts.as_slice() else { panic!("{acts:?}") };
|
||||
assert_eq!(
|
||||
(cmd.as_str(), p["center"].clone(), p["count"].clone(), p["height"].clone()),
|
||||
("addon.pathtools.shape.create", json!([50.0, 400.0]), json!(7), json!(100.0))
|
||||
);
|
||||
// A click without a drag draws nothing; Escape drops a drag.
|
||||
t.pointer(&cx, &ev(PointerKind::Down, 50.0, 50.0));
|
||||
assert_eq!(t.pointer(&cx, &ev(PointerKind::Up, 50.0, 50.0)), vec![Action::Cancel]);
|
||||
t.pointer(&cx, &ev(PointerKind::Down, 50.0, 50.0));
|
||||
assert_eq!(t.key(&cx, ToolKey::Escape, Mods::default()), vec![Action::Cancel]);
|
||||
assert_eq!(t.cursor(&cx, Point::ZERO, Mods::default()), Cursor::ParametricShape);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_kinds_arrow_keys_step_within_range() {
|
||||
let s = ParametricShape::new(ShapeKind::Star, Point::ZERO, 10.0, 10.0);
|
||||
assert_eq!(stepped(s, Field::Count, false, 1.0).count, 4);
|
||||
assert_eq!(stepped(ParametricShape { count: 3, ..s }, Field::Count, false, 1.0).count, 3);
|
||||
assert_eq!(stepped(ParametricShape { count: MAX_COUNT, ..s }, Field::Count, true, 1.0).count, MAX_COUNT);
|
||||
assert!((stepped(s, Field::A, true, 1.0).a - 0.5495).abs() < 1e-9);
|
||||
assert_eq!(stepped(ParametricShape { a: 1.0, ..s }, Field::A, true, 1.0).a, 1.0);
|
||||
for kind in ShapeKind::ALL {
|
||||
let (up, side) = arrow_fields(kind);
|
||||
let s = ParametricShape::new(kind, Point::ZERO, 10.0, 10.0);
|
||||
for f in [up, side].into_iter().flatten() {
|
||||
assert_eq!(stepped(s, f, true, 1.0).check(), Ok(()), "{kind:?} {f:?}");
|
||||
assert_eq!(stepped(s, f, false, 1.0).check(), Ok(()), "{kind:?} {f:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_click_selects_a_shape_and_its_handles_drag_into_set() {
|
||||
let (d, id) = doc_with_star();
|
||||
let p = paint();
|
||||
let none = Selection::default();
|
||||
let mut t = ParametricShapeTool::default();
|
||||
let acts = t.pointer(&cx(&d, &none, &p), &ev(PointerKind::Down, 250.0, 250.0));
|
||||
assert_eq!(acts, vec![Action::Exec("select.set".into(), json!({ "ids": [id.0] }))]);
|
||||
assert!(t.overlays(&cx(&d, &none, &p)).is_empty());
|
||||
let mut sel = Selection::default();
|
||||
sel.set([id]);
|
||||
let cx = cx(&d, &sel, &p);
|
||||
let ov = t.overlays(&cx);
|
||||
assert!(matches!(ov.first(), Some(Overlay::Path { dashed: true, .. })) && ov.len() >= 6, "{ov:?}");
|
||||
// The size handle, at the box's lower right corner: 150 × 120 now.
|
||||
assert_eq!(t.pointer(&cx, &ev(PointerKind::Down, 300.0, 300.0)), vec![Action::Begin(EDIT.into())]);
|
||||
let (cmd, p) = preview_of(&t.pointer(&cx, &ev(PointerKind::Drag, 325.0, 310.0)));
|
||||
assert_eq!(
|
||||
(cmd.as_str(), p["id"].clone(), p["width"].clone(), p["height"].clone()),
|
||||
("addon.pathtools.shape.set", json!(id.0), json!(150.0), json!(120.0))
|
||||
);
|
||||
assert!(matches!(t.pointer(&cx, &ev(PointerKind::Up, 325.0, 310.0)).as_slice(), [Action::Preview(..), Action::Commit]));
|
||||
// The centre moves it.
|
||||
t.pointer(&cx, &ev(PointerKind::Down, 251.0, 249.0));
|
||||
let (_, p) = preview_of(&t.pointer(&cx, &ev(PointerKind::Drag, 261.0, 229.0)));
|
||||
assert_eq!(p["center"], json!([260.0, 230.0]));
|
||||
t.key(&cx, ToolKey::Escape, Mods::default());
|
||||
// The rotation handle above the top.
|
||||
t.pointer(&cx, &ev(PointerKind::Down, 250.0, 200.0 - STEM_PX));
|
||||
let (_, p) = preview_of(&t.pointer(&cx, &ev(PointerKind::Drag, 350.0, 250.0)));
|
||||
assert_eq!(p["rotation"], json!(90.0));
|
||||
t.key(&cx, ToolKey::Escape, Mods::default());
|
||||
// The star's inner radius, on its first inner corner.
|
||||
let inner = Point::new(250.0, 250.0) + Vec2::new((36f64).to_radians().sin(), -(36f64).to_radians().cos()) * 25.0;
|
||||
t.pointer(&cx, &ev(PointerKind::Down, inner.x, inner.y));
|
||||
let (_, p) = preview_of(&t.pointer(&cx, &ev(PointerKind::Drag, 250.0, 210.0)));
|
||||
assert!((p["a"].as_f64().unwrap() - 0.8).abs() < 1e-9, "{p}");
|
||||
t.key(&cx, ToolKey::Escape, Mods::default());
|
||||
// The corner radius on the top edge.
|
||||
t.pointer(&cx, &ev(PointerKind::Down, 200.0, 200.0));
|
||||
let (_, p) = preview_of(&t.pointer(&cx, &ev(PointerKind::Drag, 212.0, 200.0)));
|
||||
assert_eq!(p["radius"], json!(12.0));
|
||||
// Pressing and letting go of a handle without moving changes nothing.
|
||||
t.key(&cx, ToolKey::Escape, Mods::default());
|
||||
t.pointer(&cx, &ev(PointerKind::Down, 300.0, 300.0));
|
||||
assert_eq!(t.pointer(&cx, &ev(PointerKind::Up, 300.0, 300.0)), vec![Action::Cancel]);
|
||||
// A double-click on a shape places none.
|
||||
assert!(t.pointer(&cx, &ev(PointerKind::DoubleClick, 250.0, 250.0)).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stored_numbers_are_read_back_only_while_the_path_is_the_shape() {
|
||||
let (mut d, id) = doc_with_star();
|
||||
let s = read(d.node(id).unwrap()).unwrap();
|
||||
assert_eq!((s.kind, s.center, s.count), (ShapeKind::Star, Point::new(250.0, 250.0), 5));
|
||||
// Moved: still the shape, about its new centre.
|
||||
d.node_mut(id).unwrap().path_data_mut().unwrap().transform(Affine::translate((10.0, 0.0)));
|
||||
assert_eq!(read(d.node(id).unwrap()).map(|s| s.center), Some(Point::new(260.0, 250.0)));
|
||||
// An anchor moved: a plain path.
|
||||
d.node_mut(id).unwrap().path_data_mut().unwrap().anchor_mut(0, 2).unwrap().p.x += 3.0;
|
||||
assert_eq!(read(d.node(id).unwrap()), None);
|
||||
// Damaged entries are no shape.
|
||||
for bad in ["", "{", "[]", r#"{"kind":"blob"}"#, r#"{"kind":"star","count":5000,"fp":"0"}"#, r#"{"kind":"star"}"#] {
|
||||
let mut n = d.node(id).unwrap().clone();
|
||||
n.edit_attrs(|a| a.data = vec![(KEY.into(), bad.into())]);
|
||||
assert_eq!(read(&n), None, "{bad}");
|
||||
}
|
||||
let plain = Node::path(NodeId(99), PathData::default(), Appearance::default_art());
|
||||
assert_eq!(read(&plain), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn options_set_the_next_shape_and_refuse_bad_values() {
|
||||
let mut t = ParametricShapeTool::default();
|
||||
t.set_option("kind", &json!("gear"));
|
||||
t.set_option("count", &json!(20));
|
||||
t.set_option("count", &json!(5000));
|
||||
t.set_option("a", &json!(0.3));
|
||||
t.set_option("width", &json!(-4));
|
||||
t.set_option("kind", &json!("cloud"));
|
||||
let o = t.options();
|
||||
assert_eq!((o["kind"].clone(), o["count"].clone(), o["a"].clone(), o["width"].clone()), (json!("gear"), json!(20), json!(0.3), json!(100.0)));
|
||||
assert!(o.get("center").is_none());
|
||||
assert!(apply(t.next, &json!({"center": [1, "x"]})).is_err());
|
||||
assert_eq!(apply(t.next, &json!({"rotation": 450})).map(|s| s.rotation), Ok(90.0));
|
||||
}
|
||||
}
|
||||
@@ -177,6 +177,9 @@ pub enum Cursor {
|
||||
ExtendPath,
|
||||
/// The Measure & Inspect tool (Path Precision Tools add-on).
|
||||
MeasureInspect,
|
||||
/// The Parametric Shape Tool (Path Precision Tools add-on): a drag draws a shape out from its
|
||||
/// centre; a selected shape's handles edit it.
|
||||
ParametricShape,
|
||||
/// The Smart Pen Tool (Smart Pen add-on) away from paths, where a click starts a new path (over
|
||||
/// a path or while drawing it shows the Pen's and Direct Selection's cursors).
|
||||
SmartPen,
|
||||
@@ -274,6 +277,7 @@ impl Cursor {
|
||||
| Cursor::AnchorEraser
|
||||
| Cursor::ExtendPath
|
||||
| Cursor::MeasureInspect
|
||||
| Cursor::ParametricShape
|
||||
| Cursor::SmartPen => Cursor::Crosshair,
|
||||
c => c,
|
||||
}
|
||||
@@ -311,7 +315,8 @@ impl Cursor {
|
||||
| Cursor::Scissors
|
||||
| Cursor::AnchorEraser
|
||||
| Cursor::ExtendPath
|
||||
| Cursor::MeasureInspect => Some(Cursor::Crosshair),
|
||||
| Cursor::MeasureInspect
|
||||
| Cursor::ParametricShape => Some(Cursor::Crosshair),
|
||||
Cursor::TypeVertical => Some(Cursor::Text),
|
||||
Cursor::MeshAdd | Cursor::PuppetPin => Some(Cursor::PenAdd),
|
||||
Cursor::SmartPen => Some(Cursor::Pen),
|
||||
|
||||
@@ -696,6 +696,12 @@ group = "Drawing and editing tools"
|
||||
shown_when = ["The Measure & Inspect tool (Path Precision Tools add-on): drag to measure distance and angle; hover a path to read its lengths, area, curvature and angles."]
|
||||
hotspot = "Exact, because precision matters here (an open centre suits it)."
|
||||
|
||||
[[cursor]]
|
||||
id = "ParametricShape"
|
||||
group = "Drawing and editing tools"
|
||||
shown_when = ["The Parametric Shape Tool (Path Precision Tools add-on): a drag draws a re-editable shape out from its centre; a click selects one and shows its handles."]
|
||||
hotspot = "Exact: the centre of the shape being drawn."
|
||||
|
||||
# ── Symbolism tools ───────────────────────────────────────────────────────────────────────────────
|
||||
# The brush size is drawn by the app as a circle around the cursor.
|
||||
|
||||
|
||||
@@ -70,6 +70,8 @@ const ROWS: &[Row] = &[
|
||||
(&[crate::addons::anchor_eraser::ID], None, &["size", "tolerance", "pressure"]),
|
||||
(&[crate::addons::extend_path::ID], None, &["mode"]),
|
||||
(&[crate::addons::smart_pen::ID], None, &["next"]),
|
||||
// The kind first: a new kind resets the values after it.
|
||||
(&[crate::addons::parametric_shape::ID], None, &["kind", "count", "a", "b", "radius", "width", "height", "rotation"]),
|
||||
// Flare Tool Options (`extra::FLARE_OPTIONS` and the Rays and Rings checkboxes).
|
||||
(
|
||||
&["flare"],
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
//! Path Precision Tools in the interface: its submenu (Object › Plug-ins › Path Precision Tools),
|
||||
//! which selects its tools, opens the Anchor Inspector and runs Split / Add Points at Crossings;
|
||||
//! the Anchor Inspector panel (the selected anchor's position and handles as numbers, and the
|
||||
//! add-on's anchor edits); and its tools' options dialog (a double-click on the tool). Everything
|
||||
//! acts through `addon.pathtools.*` and `tool.setOption`.
|
||||
//! which selects its tools, opens its panels and runs Split / Add Points at Crossings and Convert
|
||||
//! to Parametric Shape; the Anchor Inspector panel (the selected anchor's position and handles as
|
||||
//! numbers, and the add-on's anchor edits); the Parametric Shape panel ([`shape`]); and its tools'
|
||||
//! options dialog (a double-click on the tool). Everything acts through `addon.pathtools.*` and
|
||||
//! `tool.setOption`.
|
||||
|
||||
pub mod shape;
|
||||
|
||||
use egui::Ui;
|
||||
use serde_json::{Map, Value, json};
|
||||
use vectorcraft_geom::Point;
|
||||
use vectorcraft_tools::addons::{anchor_eraser, extend_path, measure_inspect};
|
||||
use vectorcraft_tools::addons::{anchor_eraser, extend_path, measure_inspect, parametric_shape};
|
||||
|
||||
use crate::VectorcraftApp;
|
||||
use crate::dialogs::DialogSpec;
|
||||
@@ -33,11 +36,14 @@ pub fn menu() -> Vec<Item> {
|
||||
tool("Anchor Eraser Brush", anchor_eraser::ID),
|
||||
tool("Extend Path Tool", extend_path::ID),
|
||||
tool("Measure & Inspect Tool", measure_inspect::ID),
|
||||
tool("Parametric Shape Tool", parametric_shape::ID),
|
||||
Item::Sep,
|
||||
Item::Cmd("Anchor Inspector", "window.panel", json!({ "panel": ID })),
|
||||
Item::Cmd("Parametric Shape", "window.panel", json!({ "panel": shape::ID })),
|
||||
Item::Sep,
|
||||
Item::Cmd("Add Points at Crossings", "addon.pathtools.crossings", json!({})),
|
||||
Item::Cmd("Split at Crossings", "addon.pathtools.crossings", json!({ "cut": true })),
|
||||
Item::Cmd("Convert to Parametric Shape", "addon.pathtools.shape.convert", json!({})),
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
//! The Parametric Shape panel (Path Precision Tools): the selected parametric shape's numbers as
|
||||
//! fields (kind, count, size, rotation, the kind's own values, corner radius), edited with
|
||||
//! `addon.pathtools.shape.set`; with no shape selected, the same fields set the Parametric Shape
|
||||
//! Tool's next shape (`tool.setOption`). Convert turns selected plain shapes into parametric ones.
|
||||
|
||||
use egui::Ui;
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_geom::Point;
|
||||
use vectorcraft_tools::addons::parametric_shape::{self as tool, MAX_COUNT, MAX_SIZE, MIN_COUNT, ParametricShape, ShapeKind, apply, read};
|
||||
|
||||
use crate::VectorcraftApp;
|
||||
use crate::panels::empty_state;
|
||||
use crate::widgets::{self, dim_label};
|
||||
|
||||
/// The panel's id (`window.panel {panel: "parametricShape"}`).
|
||||
pub const ID: &str = "parametricShape";
|
||||
const LABEL_W: f32 = 96.0;
|
||||
const FIELD_W: f32 = 72.0;
|
||||
|
||||
/// The kind's name in the interface.
|
||||
fn kind_label(kind: ShapeKind) -> &'static str {
|
||||
match kind {
|
||||
ShapeKind::Rectangle => "Rectangle",
|
||||
ShapeKind::Ellipse => "Ellipse",
|
||||
ShapeKind::Polygon => "Polygon",
|
||||
ShapeKind::Star => "Star",
|
||||
ShapeKind::Ring => "Ring",
|
||||
ShapeKind::Arrow => "Arrow",
|
||||
ShapeKind::Cross => "Cross",
|
||||
ShapeKind::Heart => "Heart",
|
||||
ShapeKind::Gear => "Gear",
|
||||
ShapeKind::Bubble => "Speech Bubble",
|
||||
}
|
||||
}
|
||||
|
||||
/// The labels of the kind's own values `a` and `b`.
|
||||
fn value_labels(kind: ShapeKind) -> [&'static str; 2] {
|
||||
match kind {
|
||||
ShapeKind::Star | ShapeKind::Ring => ["Inner Radius:", ""],
|
||||
ShapeKind::Arrow => ["Head:", "Shaft:"],
|
||||
ShapeKind::Cross => ["Offset:", "Thickness:"],
|
||||
ShapeKind::Heart => ["Depth:", "Point:"],
|
||||
ShapeKind::Gear => ["Tooth Height:", ""],
|
||||
ShapeKind::Bubble => ["Tail:", "Tip:"],
|
||||
ShapeKind::Rectangle | ShapeKind::Ellipse | ShapeKind::Polygon => ["", ""],
|
||||
}
|
||||
}
|
||||
|
||||
fn run(app: &mut VectorcraftApp, id: &str, p: Value) {
|
||||
if let Err(e) = app.run(id, p) {
|
||||
app.status(e);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let Some(st) = app.session.active() else {
|
||||
empty_state(ui, "hexagon", tl!("No document"), tl!("Open a document to draw parametric shapes."));
|
||||
return;
|
||||
};
|
||||
// The selected shape, else the tool's next one.
|
||||
let selected = match st.selection.objects.as_slice() {
|
||||
[id] => st.doc.node(*id).and_then(read).map(|s| (*id, s)),
|
||||
_ => None,
|
||||
};
|
||||
let plain_selected = !st.selection.is_empty() && selected.is_none();
|
||||
let fallback = ParametricShape::new(ShapeKind::Polygon, Point::ZERO, 100.0, 100.0);
|
||||
let s = selected.map(|(_, s)| s).unwrap_or_else(|| apply(fallback, &app.session.tool_options_of(tool::ID)).unwrap_or(fallback));
|
||||
let unit = app.session.general_unit();
|
||||
if selected.is_none() {
|
||||
dim_label(ui, tl!("Select a parametric shape to edit it. With none selected, these set the next shape."));
|
||||
}
|
||||
let mut change: Option<Value> = None;
|
||||
let labels = ShapeKind::ALL.map(kind_label);
|
||||
widgets::label_row(ui, "Shape:", LABEL_W, |ui| {
|
||||
if let Some(k) = widgets::dropdown(ui, "parametric-shape-kind", kind_label(s.kind), &labels, 150.0).and_then(|i| ShapeKind::ALL.get(i)) {
|
||||
change = Some(json!({ "kind": k.id() }));
|
||||
}
|
||||
});
|
||||
if s.kind.counted() {
|
||||
widgets::label_row(ui, "Count:", LABEL_W, |ui| {
|
||||
let range = f64::from(MIN_COUNT)..=f64::from(MAX_COUNT);
|
||||
if let Some(v) = widgets::range_field(ui, "parametric-shape-count", f64::from(s.count), range, "", 0, FIELD_W) {
|
||||
change = Some(json!({ "count": v }));
|
||||
}
|
||||
});
|
||||
}
|
||||
// Width and height side by side, as the Transform panel has them.
|
||||
egui::Grid::new("parametric-shape-size").num_columns(4).spacing([4.0, 6.0]).min_col_width(0.0).show(ui, |ui| {
|
||||
for (label, key, v) in [("W:", "width", s.width), ("H:", "height", s.height)] {
|
||||
dim_label(ui, label);
|
||||
if let Some(v) = widgets::size_field_named(ui, ("parametric-shape", key), Some(tl!(label)), Some(v), unit, FIELD_W) {
|
||||
change = Some(json!({ key: v }));
|
||||
}
|
||||
}
|
||||
ui.end_row();
|
||||
});
|
||||
widgets::label_row(ui, "Rotation:", LABEL_W, |ui| {
|
||||
if let Some(v) = widgets::mixed_field(ui, "parametric-shape-rotation", Some(s.rotation), "°", 1, FIELD_W) {
|
||||
change = Some(json!({ "rotation": v }));
|
||||
}
|
||||
});
|
||||
// The kind's own values, as percentages of their fraction.
|
||||
for ((key, v), (range, label)) in [("a", s.a), ("b", s.b)].into_iter().zip(s.kind.values().into_iter().zip(value_labels(s.kind))) {
|
||||
let Some(r) = range else { continue };
|
||||
widgets::label_row(ui, label, LABEL_W, |ui| {
|
||||
if let Some(x) = widgets::range_field(ui, ("parametric-shape", key), v * 100.0, r.min * 100.0..=r.max * 100.0, "%", 0, FIELD_W) {
|
||||
change = Some(json!({ key: x / 100.0 }));
|
||||
}
|
||||
});
|
||||
}
|
||||
if s.kind.rounds() {
|
||||
widgets::label_row(ui, "Corner Radius:", LABEL_W, |ui| {
|
||||
if let Some(v) = widgets::size_field(ui, "parametric-shape-radius", Some(s.radius), unit, FIELD_W) {
|
||||
change = Some(json!({ "radius": v.clamp(0.0, MAX_SIZE) }));
|
||||
}
|
||||
});
|
||||
}
|
||||
widgets::divider(ui);
|
||||
let width = ui.available_width();
|
||||
let convert = ui.add_enabled_ui(plain_selected, |ui| widgets::flat_button(ui, tl!("Convert to Parametric Shape"), width).clicked()).inner;
|
||||
match (change, selected) {
|
||||
(Some(mut p), Some((id, _))) => {
|
||||
p["id"] = json!(id.0);
|
||||
run(app, "addon.pathtools.shape.set", p);
|
||||
}
|
||||
(Some(values), None) => run(app, "tool.setOption", json!({ "tool": tool::ID, "values": values })),
|
||||
(None, _) => {}
|
||||
}
|
||||
if convert {
|
||||
run(app, "addon.pathtools.shape.convert", json!({}));
|
||||
}
|
||||
}
|
||||
@@ -16,9 +16,12 @@ fn the_plugins_submenu_selects_the_tools_and_opens_the_anchor_inspector() {
|
||||
let mut app = app();
|
||||
let entries = menus::menu_entries(&app);
|
||||
let under = |label: &str| entries.iter().find(|e| e.path == ["Object", "Plug-ins", "Path Precision Tools"] && e.label == label);
|
||||
for (label, tool) in
|
||||
[("Anchor Eraser Brush", "anchorEraserBrush"), ("Extend Path Tool", "extendPath"), ("Measure & Inspect Tool", "measureInspect")]
|
||||
{
|
||||
for (label, tool) in [
|
||||
("Anchor Eraser Brush", "anchorEraserBrush"),
|
||||
("Extend Path Tool", "extendPath"),
|
||||
("Measure & Inspect Tool", "measureInspect"),
|
||||
("Parametric Shape Tool", "parametricShape"),
|
||||
] {
|
||||
let e = under(label).expect(label);
|
||||
assert_eq!((e.command.as_deref(), &e.params), (Some("tool.select"), &json!({ "tool": tool })));
|
||||
menus::invoke(&mut app, "tool.select", e.params.clone());
|
||||
@@ -84,3 +87,29 @@ fn double_clicking_the_brush_opens_its_options_and_ok_sets_them() {
|
||||
assert_eq!(app.session.tool_options_of("extendPath")["mode"], json!("arc"));
|
||||
assert!(app.run("tool.options", json!({"tool": "measureInspect"})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_parametric_shape_panel_opens_from_the_submenu_and_shows_the_shape() {
|
||||
let mut app = app();
|
||||
let entries = menus::menu_entries(&app);
|
||||
let under = |label: &str| {
|
||||
let e = entries.iter().find(|e| e.path == ["Object", "Plug-ins", "Path Precision Tools"] && e.label == label).expect(label);
|
||||
(e.command.clone(), e.params.clone())
|
||||
};
|
||||
assert_eq!(under("Convert to Parametric Shape").0.as_deref(), Some("addon.pathtools.shape.convert"));
|
||||
let (command, params) = under("Parametric Shape");
|
||||
assert_eq!(command.as_deref(), Some("window.panel"));
|
||||
menus::invoke(&mut app, "window.panel", params);
|
||||
assert!(crate::panel_docking::showing(&app.ui, super::shape::ID), "the panel opens");
|
||||
// Nothing selected: the fields set the tool's next shape.
|
||||
let text = painted_text(&mut app, super::shape::show);
|
||||
assert!(text.contains("With none selected") && text.contains("Polygon") && text.contains("Count:") && text.contains("Corner Radius:"), "{text}");
|
||||
app.run("tool.setOption", json!({"tool": "parametricShape", "values": {"kind": "gear"}})).unwrap();
|
||||
let text = painted_text(&mut app, super::shape::show);
|
||||
assert!(text.contains("Gear") && text.contains("Tooth Height:"), "{text}");
|
||||
// A shape selected: its own numbers.
|
||||
app.run("addon.pathtools.shape.create", json!({"kind": "bubble", "center": [150, 150]})).unwrap();
|
||||
let text = painted_text(&mut app, super::shape::show);
|
||||
assert!(text.contains("Speech Bubble") && text.contains("Tail:") && text.contains("Tip:") && !text.contains("With none selected"), "{text}");
|
||||
assert!(!text.contains("Count:"), "a speech bubble has no count: {text}");
|
||||
}
|
||||
|
||||
@@ -151,6 +151,7 @@ pub(super) fn cursor_icon(c: Cursor) -> egui::CursorIcon {
|
||||
| Cursor::AnchorEraser
|
||||
| Cursor::ExtendPath
|
||||
| Cursor::MeasureInspect
|
||||
| Cursor::ParametricShape
|
||||
| Cursor::SmartPen => cursor_icon(c.stand_in().unwrap_or(Cursor::Crosshair)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -440,7 +440,7 @@ fn magnifier(p: &mut Ink, o: Pos2, out: bool) {
|
||||
|
||||
/// Every cursor with a glyph: its own, drawn here, or the one it borrows.
|
||||
#[cfg(test)]
|
||||
const GLYPHS: [Cursor; 85] = [
|
||||
const GLYPHS: [Cursor; 86] = [
|
||||
Cursor::Hand,
|
||||
Cursor::HandGrab,
|
||||
Cursor::ZoomIn,
|
||||
@@ -525,6 +525,7 @@ const GLYPHS: [Cursor; 85] = [
|
||||
Cursor::AnchorEraser,
|
||||
Cursor::ExtendPath,
|
||||
Cursor::MeasureInspect,
|
||||
Cursor::ParametricShape,
|
||||
Cursor::SmartPen,
|
||||
];
|
||||
|
||||
|
||||
@@ -379,7 +379,7 @@ fn drop_targets_follow_the_buttons_edges() {
|
||||
fn addon_tools_are_in_the_drawer_not_the_toolbars() {
|
||||
let mut app = app();
|
||||
let addon: Vec<&str> = vectorcraft_tools::addons::GROUPS.iter().flat_map(|g| g.iter().map(|t| t.id)).collect();
|
||||
assert_eq!(addon, ["anchorEraserBrush", "extendPath", "measureInspect", "smartPen"]);
|
||||
assert_eq!(addon, ["anchorEraserBrush", "extendPath", "measureInspect", "parametricShape", "smartPen"]);
|
||||
for advanced in [false, true] {
|
||||
if advanced {
|
||||
app.run("window.toolbarAdvanced", json!({})).unwrap();
|
||||
@@ -389,7 +389,7 @@ fn addon_tools_are_in_the_drawer_not_the_toolbars() {
|
||||
let listed = |id: &str| {
|
||||
sections().iter().find(|(_, tools)| tools.iter().any(|t| t.id == id)).map(|(h, tools)| (*h, tools.iter().map(|t| t.id).collect::<Vec<_>>()))
|
||||
};
|
||||
assert_eq!(listed("extendPath"), Some(("Path Precision Tools", addon[..3].to_vec())));
|
||||
assert_eq!(listed("extendPath"), Some(("Path Precision Tools", addon[..4].to_vec())));
|
||||
assert_eq!(listed("smartPen"), Some(("Smart Pen", vec!["smartPen"])));
|
||||
for tool in ["extendPath", "smartPen"] {
|
||||
app.run("toolbar.addTool", json!({"tool": tool})).unwrap();
|
||||
|
||||
@@ -5687,6 +5687,32 @@ anchor Points: Точки:
|
||||
@msg select open paths to close изберете отворени пътища за затваряне
|
||||
@msg the paths are too many or too tangled to find where they cross (at most 10000 segments) пътищата са твърде много или твърде заплетени, за да се намерят пресичанията им (най-много 10000 сегмента)
|
||||
@msg the paths have too many anchors to edit at once пътищата имат твърде много опорни точки, за да се редактират наведнъж
|
||||
# Path Precision Tools add-on: the Parametric Shape Tool, its panel, commands and messages.
|
||||
Parametric Shape Tool Инструмент „Параметрична форма“
|
||||
Parametric Shape Параметрична форма
|
||||
Edit Parametric Shape Редактиране на параметрична форма
|
||||
Convert to Parametric Shape Преобразуване в параметрична форма
|
||||
Ring Пръстен
|
||||
Heart Сърце
|
||||
Gear Зъбно колело
|
||||
Speech Bubble Балонче за реч
|
||||
Count: Брой:
|
||||
Thickness: Дебелина:
|
||||
Inner Radius: Вътрешен радиус:
|
||||
Head: Глава:
|
||||
Shaft: Стъбло:
|
||||
Point: Връх:
|
||||
Tooth Height: Височина на зъба:
|
||||
Tail: Опашка:
|
||||
Tip: Връх:
|
||||
Open a document to draw parametric shapes. Отворете документ, за да рисувате параметрични форми.
|
||||
Select a parametric shape to edit it. With none selected, these set the next shape. Изберете параметрична форма, за да я редактирате. Без избрана форма тези стойности важат за следващата.
|
||||
@msg the object is not an editable parametric shape обектът не е редактируема параметрична форма
|
||||
@msg select plain rectangles, ellipses or regular polygons to convert изберете обикновени правоъгълници, елипси или правилни многоъгълници за преобразуване
|
||||
@msg the shape's numbers are out of range стойностите на формата са извън допустимия обхват
|
||||
@msg `kind` must be one of: rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble `kind` трябва да е едно от: rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble
|
||||
@msg `count` must be a whole number from 3 to 1000 `count` трябва да е цяло число от 3 до 1000
|
||||
@msg `{key}` must be from {lo} to {hi} `{key}` трябва да е от {lo} до {hi}
|
||||
# Smart Pen add-on (Object › Plug-ins › Smart Pen): its tool, panel, commands and messages.
|
||||
Smart Pen Интелигентно перо
|
||||
Smart Pen Tool Инструмент „Интелигентно перо“
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5788,6 +5788,32 @@ anchor Points: Punts:
|
||||
@msg select open paths to close seleccioneu traçats oberts per tancar-los
|
||||
@msg the paths are too many or too tangled to find where they cross (at most 10000 segments) massa traçats, o massa enredats, per trobar-ne els encreuaments (10000 segments com a màxim)
|
||||
@msg the paths have too many anchors to edit at once els traçats tenen massa punts d’ancoratge per editar-los alhora
|
||||
# Path Precision Tools add-on: the Parametric Shape Tool, its panel, commands and messages.
|
||||
Parametric Shape Tool Eina de forma paramètrica
|
||||
Parametric Shape Forma paramètrica
|
||||
Edit Parametric Shape Edita la forma paramètrica
|
||||
Convert to Parametric Shape Converteix en forma paramètrica
|
||||
Ring Anell
|
||||
Heart Cor
|
||||
Gear Engranatge
|
||||
Speech Bubble Bafarada
|
||||
Count: Nombre:
|
||||
Thickness: Gruix:
|
||||
Inner Radius: Radi interior:
|
||||
Head: Cap:
|
||||
Shaft: Tija:
|
||||
Point: Punta:
|
||||
Tooth Height: Alçada de la dent:
|
||||
Tail: Cua:
|
||||
Tip: Punta:
|
||||
Open a document to draw parametric shapes. Obriu un document per dibuixar formes paramètriques.
|
||||
Select a parametric shape to edit it. With none selected, these set the next shape. Seleccioneu una forma paramètrica per editar-la. Sense cap selecció, aquests valors s’apliquen a la forma següent.
|
||||
@msg the object is not an editable parametric shape l’objecte no és una forma paramètrica editable
|
||||
@msg select plain rectangles, ellipses or regular polygons to convert seleccioneu rectangles, el·lipses o polígons regulars simples per convertir-los
|
||||
@msg the shape's numbers are out of range els valors de la forma són fora de l’interval
|
||||
@msg `kind` must be one of: rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble `kind` ha de ser un de: rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble
|
||||
@msg `count` must be a whole number from 3 to 1000 `count` ha de ser un nombre enter de 3 a 1000
|
||||
@msg `{key}` must be from {lo} to {hi} `{key}` ha de ser de {lo} a {hi}
|
||||
# Smart Pen add-on (Object › Plug-ins › Smart Pen): its tool, panel, commands and messages.
|
||||
Smart Pen Ploma intel·ligent
|
||||
Smart Pen Tool Eina de ploma intel·ligent
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5797,6 +5797,32 @@ anchor Points: Body:
|
||||
@msg select open paths to close vyberte otevřené cesty k uzavření
|
||||
@msg the paths are too many or too tangled to find where they cross (at most 10000 segments) příliš mnoho nebo příliš spletených cest pro nalezení jejich křížení (nejvýše 10000 segmentů)
|
||||
@msg the paths have too many anchors to edit at once cesty mají příliš mnoho kotevních bodů na úpravu najednou
|
||||
# Path Precision Tools add-on: the Parametric Shape Tool, its panel, commands and messages.
|
||||
Parametric Shape Tool Nástroj Parametrický tvar
|
||||
Parametric Shape Parametrický tvar
|
||||
Edit Parametric Shape Upravit parametrický tvar
|
||||
Convert to Parametric Shape Převést na parametrický tvar
|
||||
Ring Prstenec
|
||||
Heart Srdce
|
||||
Gear Ozubené kolo
|
||||
Speech Bubble Bublina
|
||||
Count: Počet:
|
||||
Thickness: Tloušťka:
|
||||
Inner Radius: Vnitřní poloměr:
|
||||
Head: Hrot:
|
||||
Shaft: Dřík:
|
||||
Point: Špička:
|
||||
Tooth Height: Výška zubu:
|
||||
Tail: Ocásek:
|
||||
Tip: Špička:
|
||||
Open a document to draw parametric shapes. Otevřete dokument a kreslete parametrické tvary.
|
||||
Select a parametric shape to edit it. With none selected, these set the next shape. Vyberte parametrický tvar a upravte ho. Bez výběru tyto hodnoty platí pro další tvar.
|
||||
@msg the object is not an editable parametric shape objekt není upravitelný parametrický tvar
|
||||
@msg select plain rectangles, ellipses or regular polygons to convert vyberte k převodu obyčejné obdélníky, elipsy nebo pravidelné mnohoúhelníky
|
||||
@msg the shape's numbers are out of range hodnoty tvaru jsou mimo rozsah
|
||||
@msg `kind` must be one of: rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble `kind` musí být jedno z: rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble
|
||||
@msg `count` must be a whole number from 3 to 1000 `count` musí být celé číslo od 3 do 1000
|
||||
@msg `{key}` must be from {lo} to {hi} `{key}` musí být od {lo} do {hi}
|
||||
# Smart Pen add-on (Object › Plug-ins › Smart Pen): its tool, panel, commands and messages.
|
||||
Smart Pen Chytré pero
|
||||
Smart Pen Tool Nástroj Chytré pero
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5702,6 +5702,32 @@ anchor Points: Punkter:
|
||||
@msg select open paths to close vælg åbne stier at lukke
|
||||
@msg the paths are too many or too tangled to find where they cross (at most 10000 segments) for mange eller for sammenfiltrede stier til at finde deres krydsninger (højst 10000 segmenter)
|
||||
@msg the paths have too many anchors to edit at once stierne har for mange ankerpunkter til at blive redigeret på én gang
|
||||
# Path Precision Tools add-on: the Parametric Shape Tool, its panel, commands and messages.
|
||||
Parametric Shape Tool Parametrisk form-værktøj
|
||||
Parametric Shape Parametrisk form
|
||||
Edit Parametric Shape Rediger parametrisk form
|
||||
Convert to Parametric Shape Konverter til parametrisk form
|
||||
Ring Ring
|
||||
Heart Hjerte
|
||||
Gear Tandhjul
|
||||
Speech Bubble Taleboble
|
||||
Count: Antal:
|
||||
Thickness: Tykkelse:
|
||||
Inner Radius: Indre radius:
|
||||
Head: Hoved:
|
||||
Shaft: Skaft:
|
||||
Point: Spids:
|
||||
Tooth Height: Tandhøjde:
|
||||
Tail: Hale:
|
||||
Tip: Spids:
|
||||
Open a document to draw parametric shapes. Åbn et dokument for at tegne parametriske former.
|
||||
Select a parametric shape to edit it. With none selected, these set the next shape. Vælg en parametrisk form for at redigere den. Uden valg gælder disse værdier for den næste form.
|
||||
@msg the object is not an editable parametric shape objektet er ikke en redigerbar parametrisk form
|
||||
@msg select plain rectangles, ellipses or regular polygons to convert vælg almindelige rektangler, ellipser eller regulære polygoner at konvertere
|
||||
@msg the shape's numbers are out of range formens værdier er uden for området
|
||||
@msg `kind` must be one of: rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble `kind` skal være en af: rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble
|
||||
@msg `count` must be a whole number from 3 to 1000 `count` skal være et heltal fra 3 til 1000
|
||||
@msg `{key}` must be from {lo} to {hi} `{key}` skal være fra {lo} til {hi}
|
||||
# Smart Pen add-on (Object › Plug-ins › Smart Pen): its tool, panel, commands and messages.
|
||||
Smart Pen Smart pen
|
||||
Smart Pen Tool Smart pen-værktøj
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5380,6 +5380,34 @@ anchor Points: Punkte:
|
||||
@msg select open paths to close offene Pfade zum Schließen auswählen
|
||||
@msg the paths are too many or too tangled to find where they cross (at most 10000 segments) zu viele oder zu verschlungene Pfade, um ihre Kreuzungen zu finden (höchstens 10000 Segmente)
|
||||
@msg the paths have too many anchors to edit at once die Pfade haben zu viele Ankerpunkte, um sie auf einmal zu bearbeiten
|
||||
# Path Precision Tools add-on: the Parametric Shape Tool, its panel, commands and messages.
|
||||
Parametric Shape Tool Parametrische-Form-Werkzeug
|
||||
Parametric Shape Parametrische Form
|
||||
Edit Parametric Shape Parametrische Form bearbeiten
|
||||
Convert to Parametric Shape In parametrische Form umwandeln
|
||||
Ring Ring
|
||||
Arrow Pfeil
|
||||
Cross Kreuz
|
||||
Heart Herz
|
||||
Gear Zahnrad
|
||||
Speech Bubble Sprechblase
|
||||
Count: Anzahl:
|
||||
Thickness: Stärke:
|
||||
Inner Radius: Innenradius:
|
||||
Head: Kopf:
|
||||
Shaft: Schaft:
|
||||
Point: Spitze:
|
||||
Tooth Height: Zahnhöhe:
|
||||
Tail: Zipfel:
|
||||
Tip: Spitze:
|
||||
Open a document to draw parametric shapes. Öffnen Sie ein Dokument, um parametrische Formen zu zeichnen.
|
||||
Select a parametric shape to edit it. With none selected, these set the next shape. Wählen Sie eine parametrische Form aus, um sie zu bearbeiten. Ohne Auswahl gelten diese Werte für die nächste Form.
|
||||
@msg the object is not an editable parametric shape das Objekt ist keine bearbeitbare parametrische Form
|
||||
@msg select plain rectangles, ellipses or regular polygons to convert einfache Rechtecke, Ellipsen oder regelmäßige Polygone zum Umwandeln auswählen
|
||||
@msg the shape's numbers are out of range die Werte der Form liegen außerhalb des zulässigen Bereichs
|
||||
@msg `kind` must be one of: rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble `kind` muss eines der folgenden sein: rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble
|
||||
@msg `count` must be a whole number from 3 to 1000 `count` muss eine ganze Zahl von 3 bis 1000 sein
|
||||
@msg `{key}` must be from {lo} to {hi} `{key}` muss zwischen {lo} und {hi} liegen
|
||||
# Smart Pen add-on (Object › Plug-ins › Smart Pen): its tool, panel, commands and messages.
|
||||
Smart Pen Intelligenter Zeichenstift
|
||||
Smart Pen Tool Intelligenter-Zeichenstift-Werkzeug
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5749,6 +5749,32 @@ anchor Points: Σημεία:
|
||||
@msg select open paths to close επιλέξτε ανοιχτές διαδρομές για κλείσιμο
|
||||
@msg the paths are too many or too tangled to find where they cross (at most 10000 segments) οι διαδρομές είναι πάρα πολλές ή πολύ μπλεγμένες για να βρεθούν οι διασταυρώσεις τους (έως 10000 τμήματα)
|
||||
@msg the paths have too many anchors to edit at once οι διαδρομές έχουν πάρα πολλά σημεία αγκύρωσης για επεξεργασία με μία φορά
|
||||
# Path Precision Tools add-on: the Parametric Shape Tool, its panel, commands and messages.
|
||||
Parametric Shape Tool Εργαλείο παραμετρικού σχήματος
|
||||
Parametric Shape Παραμετρικό σχήμα
|
||||
Edit Parametric Shape Επεξεργασία παραμετρικού σχήματος
|
||||
Convert to Parametric Shape Μετατροπή σε παραμετρικό σχήμα
|
||||
Ring Δακτύλιος
|
||||
Heart Καρδιά
|
||||
Gear Γρανάζι
|
||||
Speech Bubble Συννεφάκι ομιλίας
|
||||
Count: Πλήθος:
|
||||
Thickness: Πάχος:
|
||||
Inner Radius: Εσωτερική ακτίνα:
|
||||
Head: Κεφαλή:
|
||||
Shaft: Στέλεχος:
|
||||
Point: Μύτη:
|
||||
Tooth Height: Ύψος δοντιού:
|
||||
Tail: Ουρά:
|
||||
Tip: Άκρη:
|
||||
Open a document to draw parametric shapes. Ανοίξτε ένα έγγραφο για να σχεδιάσετε παραμετρικά σχήματα.
|
||||
Select a parametric shape to edit it. With none selected, these set the next shape. Επιλέξτε ένα παραμετρικό σχήμα για να το επεξεργαστείτε. Χωρίς επιλογή, αυτές οι τιμές ισχύουν για το επόμενο σχήμα.
|
||||
@msg the object is not an editable parametric shape το αντικείμενο δεν είναι επεξεργάσιμο παραμετρικό σχήμα
|
||||
@msg select plain rectangles, ellipses or regular polygons to convert επιλέξτε απλά ορθογώνια, ελλείψεις ή κανονικά πολύγωνα για μετατροπή
|
||||
@msg the shape's numbers are out of range οι τιμές του σχήματος είναι εκτός εύρους
|
||||
@msg `kind` must be one of: rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble Το `kind` πρέπει να είναι ένα από: rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble
|
||||
@msg `count` must be a whole number from 3 to 1000 Το `count` πρέπει να είναι ακέραιος από 3 έως 1000
|
||||
@msg `{key}` must be from {lo} to {hi} Το `{key}` πρέπει να είναι από {lo} έως {hi}
|
||||
# Smart Pen add-on (Object › Plug-ins › Smart Pen): its tool, panel, commands and messages.
|
||||
Smart Pen Έξυπνη πένα
|
||||
Smart Pen Tool Έξυπνη πένα (Smart Pen Tool)
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 164 and column 5.
|
@@ -5740,6 +5740,32 @@ anchor Points: Puntos:
|
||||
@msg select open paths to close seleccione trazados abiertos que cerrar
|
||||
@msg the paths are too many or too tangled to find where they cross (at most 10000 segments) hay demasiados trazados, o están demasiado enredados, para hallar sus cruces (10000 segmentos como máximo)
|
||||
@msg the paths have too many anchors to edit at once los trazados tienen demasiados puntos de ancla para editarlos a la vez
|
||||
# Path Precision Tools add-on: the Parametric Shape Tool, its panel, commands and messages.
|
||||
Parametric Shape Tool Herramienta Forma paramétrica
|
||||
Parametric Shape Forma paramétrica
|
||||
Edit Parametric Shape Editar forma paramétrica
|
||||
Convert to Parametric Shape Convertir en forma paramétrica
|
||||
Ring Anillo
|
||||
Heart Corazón
|
||||
Gear Engranaje
|
||||
Speech Bubble Bocadillo
|
||||
Count: Cantidad:
|
||||
Thickness: Grosor:
|
||||
Inner Radius: Radio interior:
|
||||
Head: Cabeza:
|
||||
Shaft: Cuerpo:
|
||||
Point: Punta:
|
||||
Tooth Height: Altura del diente:
|
||||
Tail: Cola:
|
||||
Tip: Punta:
|
||||
Open a document to draw parametric shapes. Abra un documento para dibujar formas paramétricas.
|
||||
Select a parametric shape to edit it. With none selected, these set the next shape. Seleccione una forma paramétrica para editarla. Sin selección, estos valores se aplican a la siguiente forma.
|
||||
@msg the object is not an editable parametric shape el objeto no es una forma paramétrica editable
|
||||
@msg select plain rectangles, ellipses or regular polygons to convert seleccione rectángulos, elipses o polígonos regulares simples que convertir
|
||||
@msg the shape's numbers are out of range los valores de la forma están fuera de rango
|
||||
@msg `kind` must be one of: rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble `kind` debe ser uno de: rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble
|
||||
@msg `count` must be a whole number from 3 to 1000 `count` debe ser un número entero entre 3 y 1000
|
||||
@msg `{key}` must be from {lo} to {hi} `{key}` debe estar entre {lo} y {hi}
|
||||
# Smart Pen add-on (Object › Plug-ins › Smart Pen): its tool, panel, commands and messages.
|
||||
Smart Pen Pluma inteligente
|
||||
Smart Pen Tool Herramienta Pluma inteligente
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5828,6 +5828,32 @@ anchor Points: Pisteet:
|
||||
@msg select open paths to close valitse suljettavat avoimet polut
|
||||
@msg the paths are too many or too tangled to find where they cross (at most 10000 segments) liian monta tai liian sotkuista polkua niiden risteyskohtien löytämiseen (enintään 10000 osaa)
|
||||
@msg the paths have too many anchors to edit at once poluissa on liian monta ankkuripistettä muokattavaksi kerralla
|
||||
# Path Precision Tools add-on: the Parametric Shape Tool, its panel, commands and messages.
|
||||
Parametric Shape Tool Parametrinen muoto -työkalu
|
||||
Parametric Shape Parametrinen muoto
|
||||
Edit Parametric Shape Muokkaa parametrista muotoa
|
||||
Convert to Parametric Shape Muunna parametriseksi muodoksi
|
||||
Ring Rengas
|
||||
Heart Sydän
|
||||
Gear Hammaspyörä
|
||||
Speech Bubble Puhekupla
|
||||
Count: Määrä:
|
||||
Thickness: Paksuus:
|
||||
Inner Radius: Sisäsäde:
|
||||
Head: Kärki:
|
||||
Shaft: Varsi:
|
||||
Point: Kärki:
|
||||
Tooth Height: Hampaan korkeus:
|
||||
Tail: Häntä:
|
||||
Tip: Kärki:
|
||||
Open a document to draw parametric shapes. Avaa asiakirja piirtääksesi parametrisia muotoja.
|
||||
Select a parametric shape to edit it. With none selected, these set the next shape. Valitse parametrinen muoto muokataksesi sitä. Ilman valintaa nämä arvot koskevat seuraavaa muotoa.
|
||||
@msg the object is not an editable parametric shape objekti ei ole muokattava parametrinen muoto
|
||||
@msg select plain rectangles, ellipses or regular polygons to convert valitse muunnettavat tavalliset suorakulmiot, ellipsit tai säännölliset monikulmiot
|
||||
@msg the shape's numbers are out of range muodon arvot ovat sallitun alueen ulkopuolella
|
||||
@msg `kind` must be one of: rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble `kind` on oltava jokin seuraavista: rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble
|
||||
@msg `count` must be a whole number from 3 to 1000 `count` on oltava kokonaisluku 3–1000
|
||||
@msg `{key}` must be from {lo} to {hi} `{key}` on oltava {lo}–{hi}
|
||||
# Smart Pen add-on (Object › Plug-ins › Smart Pen): its tool, panel, commands and messages.
|
||||
Smart Pen Älykynä
|
||||
Smart Pen Tool Älykynätyökalu
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5796,6 +5796,32 @@ anchor Points: Points :
|
||||
@msg select open paths to close sélectionnez des tracés ouverts à fermer
|
||||
@msg the paths are too many or too tangled to find where they cross (at most 10000 segments) trop de tracés, ou trop enchevêtrés, pour trouver leurs croisements (10000 segments au plus)
|
||||
@msg the paths have too many anchors to edit at once les tracés ont trop de points d’ancrage pour être modifiés en une fois
|
||||
# Path Precision Tools add-on: the Parametric Shape Tool, its panel, commands and messages.
|
||||
Parametric Shape Tool Outil Forme paramétrique
|
||||
Parametric Shape Forme paramétrique
|
||||
Edit Parametric Shape Modifier la forme paramétrique
|
||||
Convert to Parametric Shape Convertir en forme paramétrique
|
||||
Ring Anneau
|
||||
Heart Cœur
|
||||
Gear Engrenage
|
||||
Speech Bubble Bulle de dialogue
|
||||
Count: Nombre :
|
||||
Thickness: Épaisseur :
|
||||
Inner Radius: Rayon intérieur :
|
||||
Head: Tête :
|
||||
Shaft: Corps :
|
||||
Point: Pointe :
|
||||
Tooth Height: Hauteur des dents :
|
||||
Tail: Queue :
|
||||
Tip: Pointe :
|
||||
Open a document to draw parametric shapes. Ouvrez un document pour dessiner des formes paramétriques.
|
||||
Select a parametric shape to edit it. With none selected, these set the next shape. Sélectionnez une forme paramétrique pour la modifier. Sans sélection, ces valeurs s’appliquent à la forme suivante.
|
||||
@msg the object is not an editable parametric shape l’objet n’est pas une forme paramétrique modifiable
|
||||
@msg select plain rectangles, ellipses or regular polygons to convert sélectionnez des rectangles, ellipses ou polygones réguliers simples à convertir
|
||||
@msg the shape's numbers are out of range les valeurs de la forme sont hors limites
|
||||
@msg `kind` must be one of: rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble `kind` doit être l’un de : rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble
|
||||
@msg `count` must be a whole number from 3 to 1000 `count` doit être un nombre entier compris entre 3 et 1000
|
||||
@msg `{key}` must be from {lo} to {hi} `{key}` doit être compris entre {lo} et {hi}
|
||||
# Smart Pen add-on (Object › Plug-ins › Smart Pen): its tool, panel, commands and messages.
|
||||
Smart Pen Plume intelligente
|
||||
Smart Pen Tool Outil Plume intelligente
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5784,6 +5784,32 @@ anchor Points: Pontok:
|
||||
@msg select open paths to close jelöljön ki lezárandó nyitott útvonalakat
|
||||
@msg the paths are too many or too tangled to find where they cross (at most 10000 segments) túl sok vagy túl összegabalyodott útvonal a kereszteződések megtalálásához (legfeljebb 10000 szakasz)
|
||||
@msg the paths have too many anchors to edit at once az útvonalaknak túl sok horgonypontjuk van az egyszerre szerkesztéshez
|
||||
# Path Precision Tools add-on: the Parametric Shape Tool, its panel, commands and messages.
|
||||
Parametric Shape Tool Paraméteres alakzat eszköz
|
||||
Parametric Shape Paraméteres alakzat
|
||||
Edit Parametric Shape Paraméteres alakzat szerkesztése
|
||||
Convert to Parametric Shape Átalakítás paraméteres alakzattá
|
||||
Ring Gyűrű
|
||||
Heart Szív
|
||||
Gear Fogaskerék
|
||||
Speech Bubble Szövegbuborék
|
||||
Count: Darabszám:
|
||||
Thickness: Vastagság:
|
||||
Inner Radius: Belső sugár:
|
||||
Head: Fej:
|
||||
Shaft: Szár:
|
||||
Point: Csúcs:
|
||||
Tooth Height: Fogmagasság:
|
||||
Tail: Csóva:
|
||||
Tip: Csúcs:
|
||||
Open a document to draw parametric shapes. Nyisson meg egy dokumentumot paraméteres alakzatok rajzolásához.
|
||||
Select a parametric shape to edit it. With none selected, these set the next shape. A szerkesztéshez jelöljön ki egy paraméteres alakzatot. Kijelölés nélkül ezek az értékek a következő alakzatra vonatkoznak.
|
||||
@msg the object is not an editable parametric shape az objektum nem szerkeszthető paraméteres alakzat
|
||||
@msg select plain rectangles, ellipses or regular polygons to convert jelöljön ki átalakítandó egyszerű téglalapokat, ellipsziseket vagy szabályos sokszögeket
|
||||
@msg the shape's numbers are out of range az alakzat értékei a megengedett tartományon kívül esnek
|
||||
@msg `kind` must be one of: rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble `kind`: ezek egyike kell: rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble
|
||||
@msg `count` must be a whole number from 3 to 1000 `count`: 3 és 1000 közötti egész szám kell
|
||||
@msg `{key}` must be from {lo} to {hi} `{key}`: {lo} és {hi} között kell
|
||||
# Smart Pen add-on (Object › Plug-ins › Smart Pen): its tool, panel, commands and messages.
|
||||
Smart Pen Okostoll
|
||||
Smart Pen Tool Okostoll eszköz
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5702,6 +5702,32 @@ anchor Points: Titik:
|
||||
@msg select open paths to close pilih jalur terbuka untuk ditutup
|
||||
@msg the paths are too many or too tangled to find where they cross (at most 10000 segments) jalur terlalu banyak atau terlalu kusut untuk menemukan persilangannya (paling banyak 10000 segmen)
|
||||
@msg the paths have too many anchors to edit at once jalur memiliki terlalu banyak titik jangkar untuk disunting sekaligus
|
||||
# Path Precision Tools add-on: the Parametric Shape Tool, its panel, commands and messages.
|
||||
Parametric Shape Tool Alat bentuk parametrik
|
||||
Parametric Shape Bentuk parametrik
|
||||
Edit Parametric Shape Sunting bentuk parametrik
|
||||
Convert to Parametric Shape Ubah menjadi bentuk parametrik
|
||||
Ring Cincin
|
||||
Heart Hati
|
||||
Gear Roda gigi
|
||||
Speech Bubble Balon ucapan
|
||||
Count: Jumlah:
|
||||
Thickness: Ketebalan:
|
||||
Inner Radius: Jari-jari dalam:
|
||||
Head: Kepala:
|
||||
Shaft: Batang:
|
||||
Point: Ujung:
|
||||
Tooth Height: Tinggi gigi:
|
||||
Tail: Ekor:
|
||||
Tip: Ujung:
|
||||
Open a document to draw parametric shapes. Buka dokumen untuk menggambar bentuk parametrik.
|
||||
Select a parametric shape to edit it. With none selected, these set the next shape. Pilih bentuk parametrik untuk menyuntingnya. Tanpa pilihan, nilai ini berlaku untuk bentuk berikutnya.
|
||||
@msg the object is not an editable parametric shape objek bukan bentuk parametrik yang dapat disunting
|
||||
@msg select plain rectangles, ellipses or regular polygons to convert pilih persegi panjang, elips, atau poligon beraturan biasa untuk diubah
|
||||
@msg the shape's numbers are out of range nilai bentuk berada di luar rentang
|
||||
@msg `kind` must be one of: rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble `kind` harus salah satu dari: rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble
|
||||
@msg `count` must be a whole number from 3 to 1000 `count` harus bilangan bulat dari 3 sampai 1000
|
||||
@msg `{key}` must be from {lo} to {hi} `{key}` harus antara {lo} dan {hi}
|
||||
# Smart Pen add-on (Object › Plug-ins › Smart Pen): its tool, panel, commands and messages.
|
||||
Smart Pen Pena pintar
|
||||
Smart Pen Tool Alat pena pintar
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2169 and column 5.
|
@@ -5414,6 +5414,34 @@ anchor Points: Punti:
|
||||
@msg select open paths to close seleziona tracciati aperti da chiudere
|
||||
@msg the paths are too many or too tangled to find where they cross (at most 10000 segments) tracciati troppi o troppo intricati per trovarne gli incroci (al massimo 10000 segmenti)
|
||||
@msg the paths have too many anchors to edit at once i tracciati hanno troppi punti di ancoraggio da modificare in una volta
|
||||
# Path Precision Tools add-on: the Parametric Shape Tool, its panel, commands and messages.
|
||||
Parametric Shape Tool Strumento Forma parametrica
|
||||
Parametric Shape Forma parametrica
|
||||
Edit Parametric Shape Modifica forma parametrica
|
||||
Convert to Parametric Shape Converti in forma parametrica
|
||||
Ring Anello
|
||||
Arrow Freccia
|
||||
Cross Croce
|
||||
Heart Cuore
|
||||
Gear Ingranaggio
|
||||
Speech Bubble Fumetto
|
||||
Count: Numero:
|
||||
Thickness: Spessore:
|
||||
Inner Radius: Raggio interno:
|
||||
Head: Testa:
|
||||
Shaft: Asta:
|
||||
Point: Punta:
|
||||
Tooth Height: Altezza denti:
|
||||
Tail: Coda:
|
||||
Tip: Punta:
|
||||
Open a document to draw parametric shapes. Apri un documento per disegnare forme parametriche.
|
||||
Select a parametric shape to edit it. With none selected, these set the next shape. Seleziona una forma parametrica per modificarla. Senza selezione, questi valori valgono per la forma successiva.
|
||||
@msg the object is not an editable parametric shape l’oggetto non è una forma parametrica modificabile
|
||||
@msg select plain rectangles, ellipses or regular polygons to convert seleziona rettangoli, ellissi o poligoni regolari semplici da convertire
|
||||
@msg the shape's numbers are out of range i valori della forma sono fuori intervallo
|
||||
@msg `kind` must be one of: rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble `kind` deve essere uno tra: rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble
|
||||
@msg `count` must be a whole number from 3 to 1000 `count` deve essere un numero intero compreso tra 3 e 1000
|
||||
@msg `{key}` must be from {lo} to {hi} `{key}` deve essere compreso tra {lo} e {hi}
|
||||
# Smart Pen add-on (Object › Plug-ins › Smart Pen): its tool, panel, commands and messages.
|
||||
Smart Pen Penna intelligente
|
||||
Smart Pen Tool Strumento Penna intelligente
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5483,6 +5483,34 @@ anchor Points: ポイント:
|
||||
@msg select open paths to close 閉じるオープンパスを選択してください
|
||||
@msg the paths are too many or too tangled to find where they cross (at most 10000 segments) パスが多すぎるか複雑すぎて交差点を見つけられません(最大 10000 セグメント)
|
||||
@msg the paths have too many anchors to edit at once パスのアンカーポイントが多すぎて一度に編集できません
|
||||
# Path Precision Tools add-on: the Parametric Shape Tool, its panel, commands and messages.
|
||||
Parametric Shape Tool パラメトリックシェイプツール
|
||||
Parametric Shape パラメトリックシェイプ
|
||||
Edit Parametric Shape パラメトリックシェイプを編集
|
||||
Convert to Parametric Shape パラメトリックシェイプに変換
|
||||
Ring リング
|
||||
Arrow 矢印
|
||||
Cross 十字
|
||||
Heart ハート
|
||||
Gear 歯車
|
||||
Speech Bubble 吹き出し
|
||||
Count: 数:
|
||||
Thickness: 太さ:
|
||||
Inner Radius: 内側の半径:
|
||||
Head: 矢じり:
|
||||
Shaft: 軸:
|
||||
Point: 先端:
|
||||
Tooth Height: 歯の高さ:
|
||||
Tail: しっぽ:
|
||||
Tip: 先端:
|
||||
Open a document to draw parametric shapes. パラメトリックシェイプを描くにはドキュメントを開いてください。
|
||||
Select a parametric shape to edit it. With none selected, these set the next shape. 編集するパラメトリックシェイプを選択してください。選択がないときは、次に描くシェイプの値になります。
|
||||
@msg the object is not an editable parametric shape オブジェクトは編集可能なパラメトリックシェイプではありません
|
||||
@msg select plain rectangles, ellipses or regular polygons to convert 変換する通常の長方形、楕円または正多角形を選択してください
|
||||
@msg the shape's numbers are out of range シェイプの値が範囲外です
|
||||
@msg `kind` must be one of: rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble `kind` には次のいずれかを指定してください:rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble
|
||||
@msg `count` must be a whole number from 3 to 1000 `count` には 3 から 1000 の整数を指定してください
|
||||
@msg `{key}` must be from {lo} to {hi} `{key}` には {lo} から {hi} を指定してください
|
||||
# Smart Pen add-on (Object › Plug-ins › Smart Pen): its tool, panel, commands and messages.
|
||||
Smart Pen スマートペン
|
||||
Smart Pen Tool スマートペンツール
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5787,6 +5787,32 @@ anchor Points: წერტილები:
|
||||
@msg select open paths to close მონიშნეთ შესაკრავი ღია ტრაექტორიები
|
||||
@msg the paths are too many or too tangled to find where they cross (at most 10000 segments) ტრაექტორიები ზედმეტად ბევრი ან ჩახლართულია გადაკვეთების საპოვნელად (მაქსიმუმ 10000 სეგმენტი)
|
||||
@msg the paths have too many anchors to edit at once ტრაექტორიებს ზედმეტად ბევრი საყრდენი წერტილი აქვს ერთბაშად დასარედაქტირებლად
|
||||
# Path Precision Tools add-on: the Parametric Shape Tool, its panel, commands and messages.
|
||||
Parametric Shape Tool პარამეტრული ფიგურის ხელსაწყო
|
||||
Parametric Shape პარამეტრული ფიგურა
|
||||
Edit Parametric Shape პარამეტრული ფიგურის რედაქტირება
|
||||
Convert to Parametric Shape პარამეტრულ ფიგურად გარდაქმნა
|
||||
Ring რგოლი
|
||||
Heart გული
|
||||
Gear კბილანა
|
||||
Speech Bubble ტექსტის ბუშტი
|
||||
Count: რაოდენობა:
|
||||
Thickness: სისქე:
|
||||
Inner Radius: შიდა რადიუსი:
|
||||
Head: თავი:
|
||||
Shaft: ღერო:
|
||||
Point: წვერი:
|
||||
Tooth Height: კბილის სიმაღლე:
|
||||
Tail: კუდი:
|
||||
Tip: წვერი:
|
||||
Open a document to draw parametric shapes. გახსენით დოკუმენტი პარამეტრული ფიგურების დასახატად.
|
||||
Select a parametric shape to edit it. With none selected, these set the next shape. მონიშნეთ პარამეტრული ფიგურა მის დასარედაქტირებლად. მონიშვნის გარეშე ეს მნიშვნელობები შემდეგ ფიგურას ეხება.
|
||||
@msg the object is not an editable parametric shape ობიექტი არ არის რედაქტირებადი პარამეტრული ფიგურა
|
||||
@msg select plain rectangles, ellipses or regular polygons to convert მონიშნეთ გარდასაქმნელი ჩვეულებრივი მართკუთხედები, ელიფსები ან წესიერი მრავალკუთხედები
|
||||
@msg the shape's numbers are out of range ფიგურის მნიშვნელობები დასაშვებ დიაპაზონს სცდება
|
||||
@msg `kind` must be one of: rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble `kind` უნდა იყოს ერთ-ერთი: rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble
|
||||
@msg `count` must be a whole number from 3 to 1000 `count` უნდა იყოს მთელი რიცხვი 3-დან 1000-მდე
|
||||
@msg `{key}` must be from {lo} to {hi} `{key}` უნდა იყოს {lo}-დან {hi}-მდე
|
||||
# Smart Pen add-on (Object › Plug-ins › Smart Pen): its tool, panel, commands and messages.
|
||||
Smart Pen ჭკვიანი კალამი
|
||||
Smart Pen Tool ჭკვიანი კალმის ხელსაწყო
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -5377,6 +5377,34 @@ anchor Points: 포인트:
|
||||
@msg select open paths to close 닫을 열린 패스를 선택하세요
|
||||
@msg the paths are too many or too tangled to find where they cross (at most 10000 segments) 패스가 너무 많거나 너무 얽혀 교차점을 찾을 수 없습니다(최대 10000개 세그먼트)
|
||||
@msg the paths have too many anchors to edit at once 패스의 앵커 포인트가 너무 많아 한 번에 편집할 수 없습니다
|
||||
# Path Precision Tools add-on: the Parametric Shape Tool, its panel, commands and messages.
|
||||
Parametric Shape Tool 파라메트릭 모양 도구
|
||||
Parametric Shape 파라메트릭 모양
|
||||
Edit Parametric Shape 파라메트릭 모양 편집
|
||||
Convert to Parametric Shape 파라메트릭 모양으로 변환
|
||||
Ring 고리
|
||||
Arrow 화살표
|
||||
Cross 십자
|
||||
Heart 하트
|
||||
Gear 톱니바퀴
|
||||
Speech Bubble 말풍선
|
||||
Count: 개수:
|
||||
Thickness: 두께:
|
||||
Inner Radius: 안쪽 반지름:
|
||||
Head: 머리:
|
||||
Shaft: 몸통:
|
||||
Point: 끝:
|
||||
Tooth Height: 톱니 높이:
|
||||
Tail: 꼬리:
|
||||
Tip: 끝:
|
||||
Open a document to draw parametric shapes. 파라메트릭 모양을 그리려면 문서를 여세요.
|
||||
Select a parametric shape to edit it. With none selected, these set the next shape. 편집할 파라메트릭 모양을 선택하세요. 선택하지 않으면 이 값은 다음 모양에 적용됩니다.
|
||||
@msg the object is not an editable parametric shape 개체가 편집 가능한 파라메트릭 모양이 아닙니다
|
||||
@msg select plain rectangles, ellipses or regular polygons to convert 변환할 일반 사각형, 타원 또는 정다각형을 선택하세요
|
||||
@msg the shape's numbers are out of range 모양의 값이 범위를 벗어났습니다
|
||||
@msg `kind` must be one of: rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble `kind`은(는) 다음 중 하나여야 합니다: rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble
|
||||
@msg `count` must be a whole number from 3 to 1000 `count`는 3에서 1000 사이의 정수여야 합니다
|
||||
@msg `{key}` must be from {lo} to {hi} `{key}`은(는) {lo}에서 {hi} 사이여야 합니다
|
||||
# Smart Pen add-on (Object › Plug-ins › Smart Pen): its tool, panel, commands and messages.
|
||||
Smart Pen 스마트 펜
|
||||
Smart Pen Tool 스마트 펜 도구
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5746,6 +5746,32 @@ anchor Points: Punten:
|
||||
@msg select open paths to close selecteer open paden om te sluiten
|
||||
@msg the paths are too many or too tangled to find where they cross (at most 10000 segments) te veel of te verwarde paden om hun kruisingen te vinden (hoogstens 10000 segmenten)
|
||||
@msg the paths have too many anchors to edit at once de paden hebben te veel ankerpunten om in één keer te bewerken
|
||||
# Path Precision Tools add-on: the Parametric Shape Tool, its panel, commands and messages.
|
||||
Parametric Shape Tool Parametrische vorm-gereedschap
|
||||
Parametric Shape Parametrische vorm
|
||||
Edit Parametric Shape Parametrische vorm bewerken
|
||||
Convert to Parametric Shape Omzetten in parametrische vorm
|
||||
Ring Ring
|
||||
Heart Hart
|
||||
Gear Tandwiel
|
||||
Speech Bubble Tekstballon
|
||||
Count: Aantal:
|
||||
Thickness: Dikte:
|
||||
Inner Radius: Binnenstraal:
|
||||
Head: Kop:
|
||||
Shaft: Schacht:
|
||||
Point: Punt:
|
||||
Tooth Height: Tandhoogte:
|
||||
Tail: Staart:
|
||||
Tip: Punt:
|
||||
Open a document to draw parametric shapes. Open een document om parametrische vormen te tekenen.
|
||||
Select a parametric shape to edit it. With none selected, these set the next shape. Selecteer een parametrische vorm om die te bewerken. Zonder selectie gelden deze waarden voor de volgende vorm.
|
||||
@msg the object is not an editable parametric shape het object is geen bewerkbare parametrische vorm
|
||||
@msg select plain rectangles, ellipses or regular polygons to convert selecteer gewone rechthoeken, ellipsen of regelmatige polygonen om om te zetten
|
||||
@msg the shape's numbers are out of range de waarden van de vorm liggen buiten het bereik
|
||||
@msg `kind` must be one of: rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble `kind` moet een van deze zijn: rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble
|
||||
@msg `count` must be a whole number from 3 to 1000 `count` moet een geheel getal van 3 tot 1000 zijn
|
||||
@msg `{key}` must be from {lo} to {hi} `{key}` moet tussen {lo} en {hi} liggen
|
||||
# Smart Pen add-on (Object › Plug-ins › Smart Pen): its tool, panel, commands and messages.
|
||||
Smart Pen Slimme pen
|
||||
Smart Pen Tool Slimme pen-gereedschap
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5652,6 +5652,32 @@ anchor Points: Punkty:
|
||||
@msg select open paths to close zaznacz otwarte ścieżki do zamknięcia
|
||||
@msg the paths are too many or too tangled to find where they cross (at most 10000 segments) za dużo ścieżek lub są zbyt splątane, by znaleźć ich skrzyżowania (najwyżej 10000 segmentów)
|
||||
@msg the paths have too many anchors to edit at once ścieżki mają za dużo punktów kontrolnych, by edytować je naraz
|
||||
# Path Precision Tools add-on: the Parametric Shape Tool, its panel, commands and messages.
|
||||
Parametric Shape Tool Narzędzie Kształt parametryczny
|
||||
Parametric Shape Kształt parametryczny
|
||||
Edit Parametric Shape Edytuj kształt parametryczny
|
||||
Convert to Parametric Shape Konwertuj na kształt parametryczny
|
||||
Ring Pierścień
|
||||
Heart Serce
|
||||
Gear Koło zębate
|
||||
Speech Bubble Dymek
|
||||
Count: Liczba:
|
||||
Thickness: Grubość:
|
||||
Inner Radius: Promień wewnętrzny:
|
||||
Head: Grot:
|
||||
Shaft: Trzon:
|
||||
Point: Czubek:
|
||||
Tooth Height: Wysokość zęba:
|
||||
Tail: Ogonek:
|
||||
Tip: Czubek:
|
||||
Open a document to draw parametric shapes. Otwórz dokument, aby rysować kształty parametryczne.
|
||||
Select a parametric shape to edit it. With none selected, these set the next shape. Zaznacz kształt parametryczny, aby go edytować. Bez zaznaczenia te wartości dotyczą następnego kształtu.
|
||||
@msg the object is not an editable parametric shape obiekt nie jest edytowalnym kształtem parametrycznym
|
||||
@msg select plain rectangles, ellipses or regular polygons to convert zaznacz zwykłe prostokąty, elipsy lub wielokąty foremne do konwersji
|
||||
@msg the shape's numbers are out of range wartości kształtu są poza zakresem
|
||||
@msg `kind` must be one of: rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble `kind` musi mieć jedną z wartości: rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble
|
||||
@msg `count` must be a whole number from 3 to 1000 `count` musi być liczbą całkowitą od 3 do 1000
|
||||
@msg `{key}` must be from {lo} to {hi} `{key}` musi mieć wartość od {lo} do {hi}
|
||||
# Smart Pen add-on (Object › Plug-ins › Smart Pen): its tool, panel, commands and messages.
|
||||
Smart Pen Inteligentne pióro
|
||||
Smart Pen Tool Narzędzie Inteligentne pióro
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5715,6 +5715,32 @@ anchor Points: Pontos:
|
||||
@msg select open paths to close selecione caminhos abertos para fechar
|
||||
@msg the paths are too many or too tangled to find where they cross (at most 10000 segments) caminhos demais, ou emaranhados demais, para encontrar seus cruzamentos (no máximo 10000 segmentos)
|
||||
@msg the paths have too many anchors to edit at once os caminhos têm pontos de ancoragem demais para editar de uma vez
|
||||
# Path Precision Tools add-on: the Parametric Shape Tool, its panel, commands and messages.
|
||||
Parametric Shape Tool Ferramenta Forma Paramétrica
|
||||
Parametric Shape Forma Paramétrica
|
||||
Edit Parametric Shape Editar Forma Paramétrica
|
||||
Convert to Parametric Shape Converter em Forma Paramétrica
|
||||
Ring Anel
|
||||
Heart Coração
|
||||
Gear Engrenagem
|
||||
Speech Bubble Balão de Fala
|
||||
Count: Quantidade:
|
||||
Thickness: Espessura:
|
||||
Inner Radius: Raio Interno:
|
||||
Head: Ponta:
|
||||
Shaft: Haste:
|
||||
Point: Bico:
|
||||
Tooth Height: Altura do Dente:
|
||||
Tail: Rabicho:
|
||||
Tip: Ponta:
|
||||
Open a document to draw parametric shapes. Abra um documento para desenhar formas paramétricas.
|
||||
Select a parametric shape to edit it. With none selected, these set the next shape. Selecione uma forma paramétrica para editá-la. Sem seleção, estes valores valem para a próxima forma.
|
||||
@msg the object is not an editable parametric shape o objeto não é uma forma paramétrica editável
|
||||
@msg select plain rectangles, ellipses or regular polygons to convert selecione retângulos, elipses ou polígonos regulares simples para converter
|
||||
@msg the shape's numbers are out of range os valores da forma estão fora do intervalo
|
||||
@msg `kind` must be one of: rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble `kind` deve ser um destes: rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble
|
||||
@msg `count` must be a whole number from 3 to 1000 `count` deve ser um número inteiro de 3 a 1000
|
||||
@msg `{key}` must be from {lo} to {hi} `{key}` deve estar entre {lo} e {hi}
|
||||
# Smart Pen add-on (Object › Plug-ins › Smart Pen): its tool, panel, commands and messages.
|
||||
Smart Pen Caneta Inteligente
|
||||
Smart Pen Tool Ferramenta Caneta Inteligente
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -5775,6 +5775,32 @@ anchor Points: Точки:
|
||||
@msg select open paths to close выделите незамкнутые контуры, чтобы замкнуть их
|
||||
@msg the paths are too many or too tangled to find where they cross (at most 10000 segments) контуров слишком много или они слишком запутаны, чтобы найти их пересечения (не более 10000 сегментов)
|
||||
@msg the paths have too many anchors to edit at once у контуров слишком много опорных точек, чтобы править их за раз
|
||||
# Path Precision Tools add-on: the Parametric Shape Tool, its panel, commands and messages.
|
||||
Parametric Shape Tool Инструмент «Параметрическая фигура»
|
||||
Parametric Shape Параметрическая фигура
|
||||
Edit Parametric Shape Изменить параметрическую фигуру
|
||||
Convert to Parametric Shape Преобразовать в параметрическую фигуру
|
||||
Ring Кольцо
|
||||
Heart Сердце
|
||||
Gear Шестерёнка
|
||||
Speech Bubble Речевой пузырь
|
||||
Count: Количество:
|
||||
Thickness: Толщина:
|
||||
Inner Radius: Внутренний радиус:
|
||||
Head: Наконечник:
|
||||
Shaft: Древко:
|
||||
Point: Острие:
|
||||
Tooth Height: Высота зуба:
|
||||
Tail: Хвостик:
|
||||
Tip: Кончик:
|
||||
Open a document to draw parametric shapes. Откройте документ, чтобы рисовать параметрические фигуры.
|
||||
Select a parametric shape to edit it. With none selected, these set the next shape. Выделите параметрическую фигуру, чтобы править её. Без выделения эти значения применяются к следующей фигуре.
|
||||
@msg the object is not an editable parametric shape объект не является редактируемой параметрической фигурой
|
||||
@msg select plain rectangles, ellipses or regular polygons to convert выделите обычные прямоугольники, эллипсы или правильные многоугольники для преобразования
|
||||
@msg the shape's numbers are out of range значения фигуры вне допустимого диапазона
|
||||
@msg `kind` must be one of: rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble `kind` должно быть одним из: rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble
|
||||
@msg `count` must be a whole number from 3 to 1000 `count` должно быть целым числом от 3 до 1000
|
||||
@msg `{key}` must be from {lo} to {hi} `{key}` должно быть от {lo} до {hi}
|
||||
# Smart Pen add-on (Object › Plug-ins › Smart Pen): its tool, panel, commands and messages.
|
||||
Smart Pen Умное перо
|
||||
Smart Pen Tool Инструмент «Умное перо»
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5821,6 +5821,32 @@ anchor Points: Punkter:
|
||||
@msg select open paths to close markera öppna banor att stänga
|
||||
@msg the paths are too many or too tangled to find where they cross (at most 10000 segments) för många eller för trassliga banor för att hitta deras korsningar (högst 10000 segment)
|
||||
@msg the paths have too many anchors to edit at once banorna har för många ankarpunkter för att redigeras på en gång
|
||||
# Path Precision Tools add-on: the Parametric Shape Tool, its panel, commands and messages.
|
||||
Parametric Shape Tool Parametrisk form-verktyget
|
||||
Parametric Shape Parametrisk form
|
||||
Edit Parametric Shape Redigera parametrisk form
|
||||
Convert to Parametric Shape Konvertera till parametrisk form
|
||||
Ring Ring
|
||||
Heart Hjärta
|
||||
Gear Kugghjul
|
||||
Speech Bubble Pratbubbla
|
||||
Count: Antal:
|
||||
Thickness: Tjocklek:
|
||||
Inner Radius: Inre radie:
|
||||
Head: Huvud:
|
||||
Shaft: Skaft:
|
||||
Point: Spets:
|
||||
Tooth Height: Tandhöjd:
|
||||
Tail: Svans:
|
||||
Tip: Spets:
|
||||
Open a document to draw parametric shapes. Öppna ett dokument för att rita parametriska former.
|
||||
Select a parametric shape to edit it. With none selected, these set the next shape. Markera en parametrisk form för att redigera den. Utan markering gäller värdena nästa form.
|
||||
@msg the object is not an editable parametric shape objektet är inte en redigerbar parametrisk form
|
||||
@msg select plain rectangles, ellipses or regular polygons to convert markera vanliga rektanglar, ellipser eller regelbundna polygoner att konvertera
|
||||
@msg the shape's numbers are out of range formens värden ligger utanför intervallet
|
||||
@msg `kind` must be one of: rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble `kind` måste vara något av: rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble
|
||||
@msg `count` must be a whole number from 3 to 1000 `count` måste vara ett heltal från 3 till 1000
|
||||
@msg `{key}` must be from {lo} to {hi} `{key}` måste vara från {lo} till {hi}
|
||||
# Smart Pen add-on (Object › Plug-ins › Smart Pen): its tool, panel, commands and messages.
|
||||
Smart Pen Smart penna
|
||||
Smart Pen Tool Smart penna-verktyget
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -5755,6 +5755,32 @@ anchor Points: Noktalar:
|
||||
@msg select open paths to close kapatılacak açık yolları seçin
|
||||
@msg the paths are too many or too tangled to find where they cross (at most 10000 segments) kesişimlerini bulmak için yollar çok fazla veya çok karışık (en fazla 10000 bölüm)
|
||||
@msg the paths have too many anchors to edit at once yolların aynı anda düzenlenemeyecek kadar çok bağlantı noktası var
|
||||
# Path Precision Tools add-on: the Parametric Shape Tool, its panel, commands and messages.
|
||||
Parametric Shape Tool Parametrik Şekil Aracı
|
||||
Parametric Shape Parametrik Şekil
|
||||
Edit Parametric Shape Parametrik Şekli Düzenle
|
||||
Convert to Parametric Shape Parametrik Şekle Dönüştür
|
||||
Ring Halka
|
||||
Heart Kalp
|
||||
Gear Dişli
|
||||
Speech Bubble Konuşma Balonu
|
||||
Count: Sayı:
|
||||
Thickness: Kalınlık:
|
||||
Inner Radius: İç Yarıçap:
|
||||
Head: Uç:
|
||||
Shaft: Gövde:
|
||||
Point: Sivri Uç:
|
||||
Tooth Height: Diş Yüksekliği:
|
||||
Tail: Kuyruk:
|
||||
Tip: Uç:
|
||||
Open a document to draw parametric shapes. Parametrik şekiller çizmek için bir belge açın.
|
||||
Select a parametric shape to edit it. With none selected, these set the next shape. Düzenlemek için bir parametrik şekil seçin. Seçim yokken bu değerler bir sonraki şekle uygulanır.
|
||||
@msg the object is not an editable parametric shape nesne düzenlenebilir bir parametrik şekil değil
|
||||
@msg select plain rectangles, ellipses or regular polygons to convert dönüştürülecek düz dikdörtgenleri, elipsleri veya düzgün çokgenleri seçin
|
||||
@msg the shape's numbers are out of range şeklin değerleri aralığın dışında
|
||||
@msg `kind` must be one of: rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble `kind` şunlardan biri olmalıdır: rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble
|
||||
@msg `count` must be a whole number from 3 to 1000 `count` 3 ile 1000 arasında bir tam sayı olmalıdır
|
||||
@msg `{key}` must be from {lo} to {hi} `{key}` {lo} ile {hi} arasında olmalıdır
|
||||
# Smart Pen add-on (Object › Plug-ins › Smart Pen): its tool, panel, commands and messages.
|
||||
Smart Pen Akıllı Kalem
|
||||
Smart Pen Tool Akıllı Kalem Aracı
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -5394,6 +5394,34 @@ anchor Points: Точки:
|
||||
@msg select open paths to close виділіть незамкнені контури, щоб замкнути їх
|
||||
@msg the paths are too many or too tangled to find where they cross (at most 10000 segments) контурів забагато або вони надто заплутані, щоб знайти їхні перетини (не більше 10000 сегментів)
|
||||
@msg the paths have too many anchors to edit at once у контурів забагато опорних точок, щоб редагувати їх за раз
|
||||
# Path Precision Tools add-on: the Parametric Shape Tool, its panel, commands and messages.
|
||||
Parametric Shape Tool Інструмент «Параметрична фігура»
|
||||
Parametric Shape Параметрична фігура
|
||||
Edit Parametric Shape Редагувати параметричну фігуру
|
||||
Convert to Parametric Shape Перетворити на параметричну фігуру
|
||||
Ring Кільце
|
||||
Arrow Стрілка
|
||||
Cross Хрест
|
||||
Heart Серце
|
||||
Gear Зубчасте колесо
|
||||
Speech Bubble Мовна бульбашка
|
||||
Count: Кількість:
|
||||
Thickness: Товщина:
|
||||
Inner Radius: Внутрішній радіус:
|
||||
Head: Наконечник:
|
||||
Shaft: Держак:
|
||||
Point: Вістря:
|
||||
Tooth Height: Висота зуба:
|
||||
Tail: Хвостик:
|
||||
Tip: Кінчик:
|
||||
Open a document to draw parametric shapes. Відкрийте документ, щоб малювати параметричні фігури.
|
||||
Select a parametric shape to edit it. With none selected, these set the next shape. Виділіть параметричну фігуру, щоб редагувати її. Без виділення ці значення стосуються наступної фігури.
|
||||
@msg the object is not an editable parametric shape об’єкт не є редагованою параметричною фігурою
|
||||
@msg select plain rectangles, ellipses or regular polygons to convert виділіть звичайні прямокутники, еліпси або правильні багатокутники для перетворення
|
||||
@msg the shape's numbers are out of range значення фігури поза допустимим діапазоном
|
||||
@msg `kind` must be one of: rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble `kind` має бути одним із: rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble
|
||||
@msg `count` must be a whole number from 3 to 1000 `count` має бути цілим числом від 3 до 1000
|
||||
@msg `{key}` must be from {lo} to {hi} `{key}` має бути від {lo} до {hi}
|
||||
# Smart Pen add-on (Object › Plug-ins › Smart Pen): its tool, panel, commands and messages.
|
||||
Smart Pen Розумне перо
|
||||
Smart Pen Tool Інструмент «Розумне перо»
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 178 and column 5.
|
@@ -5396,6 +5396,34 @@ anchor Points: Điểm:
|
||||
@msg select open paths to close chọn các đường dẫn mở để đóng
|
||||
@msg the paths are too many or too tangled to find where they cross (at most 10000 segments) quá nhiều đường dẫn hoặc chúng quá rối để tìm chỗ giao nhau (tối đa 10000 đoạn)
|
||||
@msg the paths have too many anchors to edit at once các đường dẫn có quá nhiều điểm neo để sửa cùng lúc
|
||||
# Path Precision Tools add-on: the Parametric Shape Tool, its panel, commands and messages.
|
||||
Parametric Shape Tool Công cụ Hình tham số
|
||||
Parametric Shape Hình tham số
|
||||
Edit Parametric Shape Sửa hình tham số
|
||||
Convert to Parametric Shape Chuyển thành hình tham số
|
||||
Ring Vành khuyên
|
||||
Arrow Mũi tên
|
||||
Cross Chữ thập
|
||||
Heart Trái tim
|
||||
Gear Bánh răng
|
||||
Speech Bubble Bong bóng thoại
|
||||
Count: Số lượng:
|
||||
Thickness: Độ dày:
|
||||
Inner Radius: Bán kính trong:
|
||||
Head: Đầu:
|
||||
Shaft: Thân:
|
||||
Point: Mũi:
|
||||
Tooth Height: Chiều cao răng:
|
||||
Tail: Đuôi:
|
||||
Tip: Chóp:
|
||||
Open a document to draw parametric shapes. Mở một tài liệu để vẽ hình tham số.
|
||||
Select a parametric shape to edit it. With none selected, these set the next shape. Chọn một hình tham số để sửa nó. Khi không chọn gì, các giá trị này dùng cho hình tiếp theo.
|
||||
@msg the object is not an editable parametric shape đối tượng không phải hình tham số có thể sửa
|
||||
@msg select plain rectangles, ellipses or regular polygons to convert chọn các hình chữ nhật, hình elip hoặc đa giác đều thông thường để chuyển đổi
|
||||
@msg the shape's numbers are out of range các giá trị của hình nằm ngoài phạm vi
|
||||
@msg `kind` must be one of: rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble `kind` phải là một trong: rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble
|
||||
@msg `count` must be a whole number from 3 to 1000 `count` phải là số nguyên từ 3 đến 1000
|
||||
@msg `{key}` must be from {lo} to {hi} `{key}` phải từ {lo} đến {hi}
|
||||
# Smart Pen add-on (Object › Plug-ins › Smart Pen): its tool, panel, commands and messages.
|
||||
Smart Pen Bút thông minh
|
||||
Smart Pen Tool Công cụ Bút thông minh
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 200 and column 5.
|
@@ -5637,6 +5637,34 @@ anchor Points: 点:
|
||||
@msg select open paths to close 请选择要闭合的开放路径
|
||||
@msg the paths are too many or too tangled to find where they cross (at most 10000 segments) 路径太多或太复杂,无法找到交叉处(最多 10000 段)
|
||||
@msg the paths have too many anchors to edit at once 路径的锚点太多,无法一次编辑
|
||||
# Path Precision Tools add-on: the Parametric Shape Tool, its panel, commands and messages.
|
||||
Parametric Shape Tool 参数化形状工具
|
||||
Parametric Shape 参数化形状
|
||||
Edit Parametric Shape 编辑参数化形状
|
||||
Convert to Parametric Shape 转换为参数化形状
|
||||
Ring 圆环
|
||||
Arrow 箭头
|
||||
Cross 十字
|
||||
Heart 心形
|
||||
Gear 齿轮
|
||||
Speech Bubble 对话气泡
|
||||
Count: 数量:
|
||||
Thickness: 粗细:
|
||||
Inner Radius: 内半径:
|
||||
Head: 箭头头部:
|
||||
Shaft: 箭杆:
|
||||
Point: 尖端:
|
||||
Tooth Height: 齿高:
|
||||
Tail: 尾巴:
|
||||
Tip: 尖端:
|
||||
Open a document to draw parametric shapes. 打开文档以绘制参数化形状。
|
||||
Select a parametric shape to edit it. With none selected, these set the next shape. 选择一个参数化形状以编辑它。未选择时,这些值用于下一个形状。
|
||||
@msg the object is not an editable parametric shape 该对象不是可编辑的参数化形状
|
||||
@msg select plain rectangles, ellipses or regular polygons to convert 请选择要转换的普通矩形、椭圆或正多边形
|
||||
@msg the shape's numbers are out of range 形状的数值超出范围
|
||||
@msg `kind` must be one of: rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble `kind` 必须是以下之一:rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble
|
||||
@msg `count` must be a whole number from 3 to 1000 `count` 必须是 3 到 1000 之间的整数
|
||||
@msg `{key}` must be from {lo} to {hi} `{key}` 必须在 {lo} 到 {hi} 之间
|
||||
# Smart Pen add-on (Object › Plug-ins › Smart Pen): its tool, panel, commands and messages.
|
||||
Smart Pen 智能钢笔
|
||||
Smart Pen Tool 智能钢笔工具
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5433,6 +5433,34 @@ anchor Points: 點:
|
||||
@msg select open paths to close 請選取要封閉的開放路徑
|
||||
@msg the paths are too many or too tangled to find where they cross (at most 10000 segments) 路徑太多或太複雜,無法找到交叉處(最多 10000 段)
|
||||
@msg the paths have too many anchors to edit at once 路徑的錨點太多,無法一次編輯
|
||||
# Path Precision Tools add-on: the Parametric Shape Tool, its panel, commands and messages.
|
||||
Parametric Shape Tool 參數化形狀工具
|
||||
Parametric Shape 參數化形狀
|
||||
Edit Parametric Shape 編輯參數化形狀
|
||||
Convert to Parametric Shape 轉換為參數化形狀
|
||||
Ring 圓環
|
||||
Arrow 箭頭
|
||||
Cross 十字
|
||||
Heart 心形
|
||||
Gear 齒輪
|
||||
Speech Bubble 對話氣泡
|
||||
Count: 數量:
|
||||
Thickness: 粗細:
|
||||
Inner Radius: 內半徑:
|
||||
Head: 箭頭頭部:
|
||||
Shaft: 箭桿:
|
||||
Point: 尖端:
|
||||
Tooth Height: 齒高:
|
||||
Tail: 尾巴:
|
||||
Tip: 尖端:
|
||||
Open a document to draw parametric shapes. 開啟文件以繪製參數化形狀。
|
||||
Select a parametric shape to edit it. With none selected, these set the next shape. 選取一個參數化形狀以編輯它。未選取時,這些值用於下一個形狀。
|
||||
@msg the object is not an editable parametric shape 該物件不是可編輯的參數化形狀
|
||||
@msg select plain rectangles, ellipses or regular polygons to convert 請選取要轉換的一般矩形、橢圓或正多邊形
|
||||
@msg the shape's numbers are out of range 形狀的數值超出範圍
|
||||
@msg `kind` must be one of: rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble `kind` 必須是下列其中之一:rectangle, ellipse, polygon, star, ring, arrow, cross, heart, gear, bubble
|
||||
@msg `count` must be a whole number from 3 to 1000 `count` 必須是介於 3 到 1000 的整數
|
||||
@msg `{key}` must be from {lo} to {hi} `{key}` 必須介於 {lo} 到 {hi}
|
||||
# Smart Pen add-on (Object › Plug-ins › Smart Pen): its tool, panel, commands and messages.
|
||||
Smart Pen 智慧筆型
|
||||
Smart Pen Tool 智慧筆型工具
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -182,6 +182,7 @@ pub fn show_icon_panel(app: &mut VectorcraftApp, ui: &mut Ui, id: &str) {
|
||||
crate::addons::cleanup::ID => crate::addons::cleanup::show(app, ui),
|
||||
crate::addons::color::ID => crate::addons::color::show(app, ui),
|
||||
crate::addons::pathtools::ID => crate::addons::pathtools::show(app, ui),
|
||||
crate::addons::pathtools::shape::ID => crate::addons::pathtools::shape::show(app, ui),
|
||||
crate::addons::pen::ID => crate::addons::pen::show(app, ui),
|
||||
id if library_panel::parse(id).is_some() => library_panel::show(app, ui, id),
|
||||
_ => {
|
||||
|
||||
@@ -200,6 +200,7 @@ pub const ICON_PANELS: &[(&str, &str, &str)] = &[
|
||||
(crate::addons::cleanup::ID, "Artwork Cleanup", "sparkles"),
|
||||
(crate::addons::color::ID, "Color Effects", "sun"),
|
||||
(crate::addons::pathtools::ID, "Anchor Inspector", "dc-anchor-smooth"),
|
||||
(crate::addons::pathtools::shape::ID, "Parametric Shape", "hexagon"),
|
||||
(crate::addons::pen::ID, "Smart Pen", "spline-pointer"),
|
||||
];
|
||||
|
||||
|
||||
+37
-3
@@ -5716,9 +5716,10 @@ topmost piece.
|
||||
|
||||
Path Precision Tools is a built-in add-on for exact anchor and path work: its commands are `addon.pathtools.*`, and in
|
||||
the desktop app it lives in Object › Plug-ins › Path Precision Tools, in its Anchor Inspector panel
|
||||
(`window.panel {panel: "anchorInspector"}`) and in three tools that the toolbars don't show until they are added from the
|
||||
All Tools drawer (or chosen with `tool.select`): `anchorEraserBrush`, `extendPath` and `measureInspect`. Each edit is one
|
||||
undo step, and an edit that changes nothing makes no step.
|
||||
(`window.panel {panel: "anchorInspector"}`), in its Parametric Shape panel (`window.panel {panel: "parametricShape"}`)
|
||||
and in four tools that the toolbars don't show until they are added from the All Tools drawer (or chosen with
|
||||
`tool.select`): `anchorEraserBrush`, `extendPath`, `measureInspect` and `parametricShape`. None has a shortcut. Each edit
|
||||
is one undo step, and an edit that changes nothing makes no step.
|
||||
|
||||
Tools:
|
||||
|
||||
@@ -5732,6 +5733,15 @@ Tools:
|
||||
- **Measure & Inspect tool** (`measureInspect`): a drag measures distance, angle and offsets, as the Measure tool does
|
||||
(`tool.options` reports the last one); hovering a path reads it, as `addon.pathtools.inspect` does. Nothing goes into
|
||||
the document.
|
||||
- **Parametric Shape Tool** (`parametricShape`): draws shapes that stay editable as numbers. A drag from the centre runs
|
||||
`addon.pathtools.shape.create`: the drag's length is the shape's size (height 2 × the distance, width in the last
|
||||
shape's proportions), its direction the rotation (Shift: 45° steps). While dragging, ↑/↓ change the kind's count or
|
||||
first value and ←/→ its second value or corner radius. A click selects a parametric shape and shows its handles:
|
||||
centre (move), the box's lower right corner (size; Shift keeps the proportions), a handle above the top (rotation;
|
||||
Shift: 45° steps), a dot on the top edge (corner radius) and one of the kind's own (a star's or ring's inner radius, a
|
||||
gear's tooth height, an arrow's head, a cross's bar, a heart's notch, a bubble's tail tip); each drag runs
|
||||
`addon.pathtools.shape.set`. A double-click on the canvas places a shape with the last settings. Options
|
||||
(`tool.setOption`, or the panel with no shape selected) are the next shape's numbers, as the commands take them.
|
||||
|
||||
Commands:
|
||||
|
||||
@@ -5764,11 +5774,35 @@ Commands:
|
||||
- `addon.pathtools.crossings {cut?, selfCrossings?, ids?}` adds anchors where the selected paths cross one another (and
|
||||
where each crosses itself, with `selfCrossings`), or with `cut` also cuts them there: a path of one subpath becomes a
|
||||
path per piece. Paths that meet at a shared anchor don't cross there; at most 10,000 segments at once.
|
||||
- `addon.pathtools.shape.create {center, kind?, width?, height?, rotation?, count?, a?, b?, radius?}` draws a parametric
|
||||
shape with the current paint, selected. `kind`: `rectangle`, `ellipse`, `polygon` (default), `star`, `ring`, `arrow`,
|
||||
`cross`, `heart`, `gear` or `bubble` (a speech bubble). `width` and `height` are 0.01 to 16,384 pt (100);
|
||||
`rotation` is in degrees clockwise (0: upright, a polygon's first corner up); `count` is a polygon's sides, a star's
|
||||
points or a gear's teeth (3 to 1,000); `radius` rounds the corners (0 to 16,384 pt; no corner passes the middle of a
|
||||
side). `a` and `b` are the kind's own values, fractions of its size: star, inner radius (0.01 to 1, 0.5); ring, the
|
||||
hole (0.01 to 0.99, 0.5); arrow, head length and shaft width (0.05 to 0.95, 0.4 each); cross, the bar's offset (−0.9
|
||||
to 0.9, 0; negative is up) and the arms' thickness (0.05 to 0.95, 1/3); heart, the notch's depth and the point's
|
||||
roundness (0 to 1; 0.4 and 0); gear, tooth height (0.01 to 0.9, 0.2); bubble, the tail's height (0.05 to 0.9, 0.25)
|
||||
and where its tip is across (−1 to 1, −0.4). → `{id, shape}`.
|
||||
- `addon.pathtools.shape.set {id, …}` changes a parametric shape's numbers (the same keys; a new `kind` takes its own
|
||||
defaults for the values not given) and redraws it in place. With no change it makes no step. → `{id, shape}` (its
|
||||
numbers now, so `{id}` alone reads them).
|
||||
- `addon.pathtools.shape.convert {ids?}` makes the selected plain closed paths that are rectangles, ellipses or
|
||||
polygons with equal sides and corners (stretched along their own axes at most) parametric shapes with the same
|
||||
outline. → `{ids}` (those converted).
|
||||
|
||||
A parametric shape is an ordinary path. It keeps its numbers, with a fingerprint of the outline they drew, in one
|
||||
entry of its object data (`vc-pathtools-shape`, as `document.node` lists `attrs.data`), which SVG export leaves out;
|
||||
copies and duplicates keep it. Moved, rotated or scaled with any tool it stays editable: its centre, size and rotation
|
||||
follow (a shape with rounded corners only when scaled evenly). Skewed, mirrored, or with its anchors edited, it is a
|
||||
plain path from then on.
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"addon.pathtools.extend","params":{"id":12,"end":"end","length":40,"mode":"arc"}}}
|
||||
{"name":"run_command","arguments":{"command":"addon.pathtools.erase","params":{"points":[[100,40],[100,160]],"diameter":30}}}
|
||||
{"name":"run_command","arguments":{"command":"addon.pathtools.crossings","params":{"cut":true}}}
|
||||
{"name":"run_command","arguments":{"command":"addon.pathtools.shape.create","params":{"kind":"star","center":[200,150],"width":120,"height":120,"count":7,"a":0.4}}}
|
||||
{"name":"run_command","arguments":{"command":"addon.pathtools.shape.set","params":{"id":14,"kind":"gear","count":16,"radius":2}}}
|
||||
```
|
||||
|
||||
## Smart Pen add-on
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Parity with Adobe Illustrator
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (Libraries panel: art dragged off the canvas and swatches dragged in are added where they are dropped; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (Path Precision Tools add-on: the Parametric Shape Tool; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026)
|
||||
|
||||
## Feature submission evidence
|
||||
|
||||
@@ -1229,6 +1229,19 @@ Kernels in `vectorcraft_pathops::addons::pathtools`. Tests: `addons::pathtools`
|
||||
undo step, bad params refused, the brush's stroke one step) and `edit_toolbar::tests::addon_tools_are_in_the_drawer_not_the_toolbars`.
|
||||
Commands: `docs/mcp.md` › Path Precision Tools add-on.
|
||||
|
||||
The add-on's Parametric Shape Tool (2026-10-30) draws ten kinds of shape that stay editable as numbers (rectangle,
|
||||
ellipse, polygon and star with 3 to 1,000 sides or points, ring, arrow, cross, heart, gear, speech bubble; corner radius
|
||||
limited to the middle of a side): a drag from the centre sizes and turns one, the arrow keys change its count and values
|
||||
while dragging, handles edit a selected one, a double-click places one with the last settings, and its panel edits the
|
||||
numbers. A shape is a plain path whose numbers and outline fingerprint sit in one object-data entry that SVG export
|
||||
leaves out; moving, rotating or scaling it keeps it editable, editing its anchors makes it a plain path. Convert to
|
||||
Parametric Shape recognises plain rectangles, ellipses and regular polygons. Geometry in
|
||||
`vectorcraft_pathops::addons::pathtools::shapes`. Tests: `shapes` in `vectorcraft-pathops` (each kind, ranges,
|
||||
recovery after transforms, recognition), `addons::parametric_shape` in `vectorcraft-tools` (drag, keys, handles,
|
||||
stored data), `cmd::addons::pathtools::tests` in `vectorcraft-engine` (each command one undo step, bad params refused,
|
||||
anchor edits and transforms, the SVG export filter, the tool's gestures), `addons::pathtools::tests` in
|
||||
`vectorcraft-ui-egui` (submenu and panel) and `command_sweep`.
|
||||
|
||||
## Smart Pen add-on (2026-10-30)
|
||||
|
||||
A VectorCraft addition, not an Illustrator feature, so no area score moves. Object › Plug-ins › Smart Pen selects the
|
||||
@@ -1260,6 +1273,7 @@ spot colours, carry a colour adjustment, or are RGB, CMYK or grayscale images
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-30 | minor | Path Precision Tools add-on: the Parametric Shape Tool; evidence added; scores unchanged. |
|
||||
| 2026-10-30 | minor | Presentation Mode fits the active artboard and hides selections, guides, grids and artboard edges; scores and estimates unchanged |
|
||||
| 2026-10-30 | minor | Color Effects add-on: Select by Color Kind; scores and estimates unchanged |
|
||||
| 2026-10-30 | minor | Gradient stop popover: an eyedropper samples the stop's colour from the art (#4681); scores and estimates unchanged |
|
||||
|
||||
+4
-1
@@ -1,6 +1,6 @@
|
||||
# UI and interaction parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (Gradient panel presets; View › Hide Artboards hides their edges and shows the canvas as paper (#4038); scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (Path Precision Tools add-on: the Parametric Shape Tool and its panel; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
|
||||
## Feature submission evidence
|
||||
|
||||
@@ -324,6 +324,8 @@ next unit in the order a right-click on a ruler lists them, from Feet back to Po
|
||||
| Preferences › General › Constrain Angle | done | arrow keys and Shift drags along the turned axes; the Rectangle, Rounded Rectangle, Ellipse and grid tools, point and area type and the shape size dialogs draw along them with the bounding box turned; Shift-drawn polygons and stars keep the turned base orientation; the Line Segment tool's Shift steps count from it (#394, <https://helpx.adobe.com/illustrator/desktop/manage-objects/arrange-objects/rotate-objects-by-specific-angles.html>); arcs, spirals, flares and Pen paths are not turned |
|
||||
| Tabs panel: a stop drags from where the press landed, a stop just added included | done | #1458 |
|
||||
| Outline view keeps template layers in Preview, dimmed | done | #1486; a picture placed to be traced stays visible under the outlines |
|
||||
| Smart Pen add-on (VectorCraft addition): Object › Plug-ins › Smart Pen selects the Smart Pen Tool, which the All Tools drawer lists under its own heading but neither toolbar shows as shipped (no shortcut), and opens the Smart Pen panel (next segment straight or curved; Remove Points Keep Shape, Make Smooth, Make Corner, Retract Handles, Close or Join). The tool draws (click: straight or curved segment, a curve after a straight one tangent; drag: smooth anchor, Alt breaks the handle, Shift 45°, Space moves it; Tab switches a segment; rubber band; close, join, continue) and edits without switching tools (click selects a point, drag moves it, Alt-click removes it keeping the shape, Shift+Alt-click deletes it, double-click toggles smooth/corner; handle drags with Shift and Alt; segment drags reshape, Alt-click adds a point) | done | `addon.pen.*`, `crates/tools/src/addons/smart_pen.rs`, `crates/ui-egui/src/addons/pen.rs` and their tests; not an Illustrator feature, so it is not scored |
|
||||
| Path Precision Tools add-on (VectorCraft addition): Object › Plug-ins › Path Precision Tools selects its four tools (Anchor Eraser Brush, Extend Path, Measure & Inspect, Parametric Shape), which the All Tools drawer lists under their own heading but neither toolbar shows as shipped, opens the Anchor Inspector panel (the selected anchor's X/Y and handle lengths and angles as fields; Make Smooth, Make Corner, Equalize Handles, Remove Points Keep Shape, Add Points, the three ways to close) and the Parametric Shape panel (kind, count, W/H, rotation, the kind's own values and corner radius of the selected parametric shape, or of the tool's next one), and runs Add Points at / Split at Crossings and Convert to Parametric Shape; the Parametric Shape Tool draws ten re-editable kinds from the centre (↑/↓ and ←/→ change their numbers while dragging) and edits a selected one with handles; a double-click on the brush or Extend Path tool opens its options | done | `addon.pathtools.*`, `crates/tools/src/addons/`, `crates/ui-egui/src/addons/pathtools.rs`, `crates/ui-egui/src/addons/pathtools/shape.rs` and their tests; not an Illustrator feature, so it is not scored |
|
||||
| Smart Pen add-on (VectorCraft addition): Object › Plug-ins › Smart Pen selects the Smart Pen Tool, which the All Tools drawer lists under its own heading but neither toolbar shows as shipped (no shortcut), and opens the Smart Pen panel (next segment straight or curved; Remove Points Keep Shape, Make Smooth, Make Corner, Retract Handles, Close or Join). The tool draws (click: straight or curved segment, a curve after a straight one tangent; drag: smooth anchor, Alt breaks the handle, Shift 45°, Space moves it; Tab switches a segment; rubber band; close, join, continue) and edits without switching tools (click selects a point, drag moves it, Alt-click removes it keeping the shape, Shift+Alt-click deletes it, double-click toggles smooth/corner; handle drags with Shift and Alt; segment drags reshape, Alt-click adds a point); an Alt-drag away from paths selects the points inside a marquee | done | `addon.pen.*`, `crates/tools/src/addons/smart_pen.rs`, `crates/ui-egui/src/addons/pen.rs` and their tests; not an Illustrator feature, so it is not scored |
|
||||
| Path Precision Tools add-on (VectorCraft addition): Object › Plug-ins › Path Precision Tools selects its three tools (Anchor Eraser Brush, Extend Path, Measure & Inspect), which the All Tools drawer lists under their own heading but neither toolbar shows as shipped, opens the Anchor Inspector panel (the selected anchor's X/Y and handle lengths and angles as fields; Make Smooth, Make Corner, Equalize Handles, Remove Points Keep Shape, Add Points, the three ways to close) and runs Add Points at / Split at Crossings; a double-click on the brush or Extend Path tool opens its options | done | `addon.pathtools.*`, `crates/tools/src/addons/`, `crates/ui-egui/src/addons/pathtools.rs` and their tests; not an Illustrator feature, so it is not scored |
|
||||
| Artwork Cleanup add-on (VectorCraft addition): Object › Plug-ins › Artwork Cleanup opens its panel, which checks the selection or the document for 16 kinds of issue, lists them with counts, and shows or fixes the picked rows or all of them, each fix one undo step; Simplify Anchors (Keep Shape), Rejoin Path Pieces and Deep Ungroup open their options first | done | `addon.cleanup.*`, `crates/ui-egui/src/addons/cleanup.rs` and its tests; not an Illustrator feature, so it is not scored |
|
||||
@@ -368,6 +370,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-30 | minor | Path Precision Tools add-on: the Parametric Shape Tool (ten re-editable shape kinds), its panel and Convert to Parametric Shape; scores and estimates unchanged |
|
||||
| 2026-10-30 | minor | Smart Pen add-on: an Alt-drag away from paths selects points with a marquee; scores and estimates unchanged |
|
||||
| 2026-10-30 | minor | Color Effects add-on: Select by Color Kind; scores and estimates unchanged |
|
||||
| 2026-10-30 | minor | View › Hide Artboards hides their edges and shows the canvas as paper (#4038); scores and estimates unchanged |
|
||||
|
||||
Reference in New Issue
Block a user