mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
M2.6: Flare tool (centre drag, then click for the rings; click → Flare Tool Options; shape.flare builds centre/halo/rays/rings from radial gradients in Screen mode) and Reshape tool (path.reshape: grab a point of a selected path, neighbourhood follows with a smooth falloff)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
7d95f146ce
commit
71d43421f5
6 files changed
+415
No files matched your search
@@ -12,6 +12,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
cmd!("shape.ellipse", "Ellipse", [], None, "{x, y, width, height} → {id}", has_doc, ellipse),
|
||||
cmd!("shape.polygon", "Polygon", [], None, "{cx, cy, radius, sides=6, rotation?: deg} → {id}", has_doc, polygon),
|
||||
cmd!("shape.star", "Star", [], None, "{cx, cy, radius1, radius2, points=5, rotation?: deg} → {id}", has_doc, star),
|
||||
cmd!(
|
||||
"shape.flare",
|
||||
"Flare",
|
||||
[],
|
||||
None,
|
||||
"{cx, cy, diameter=100, opacity=50 (%), brightness=30 (%), growth=20 (%), fuzziness=50 (%), rays=15, longest=300 (%), rayFuzziness=100 (%), x2?, y2? (ring end; else pathLength at direction), pathLength=300, rings=10, largest=50 (%), direction=45 (deg), seed?} → {id}",
|
||||
has_doc,
|
||||
flare
|
||||
),
|
||||
cmd!("shape.line", "Line Segment", [], None, "{x1, y1, x2, y2} → {id}", has_doc, line),
|
||||
cmd!("shape.spiral", "Spiral", [], None, "{cx, cy, radius, decay=80 (%), segments=10, clockwise?} → {id}", has_doc, spiral),
|
||||
cmd!("shape.arc", "Arc", [], None, "{x1, y1, x2, y2, closed?} → {id}", has_doc, arc),
|
||||
@@ -178,6 +187,105 @@ fn star(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
add_art(s, "Star", path_kind(path, None), None)
|
||||
}
|
||||
|
||||
/// Flare tool: a lens flare built from radial-gradient paths in Screen mode — a bright centre, a halo
|
||||
/// ring, rays of varying length and rings along the path to the end point.
|
||||
fn flare(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
use vectorcraft_color::{BlendMode, Gradient, GradientGeom, GradientKind, GradientPaint, GradientStop, Paint};
|
||||
let c = Point::new(f64_req(p, "cx", "shape.flare")?, f64_req(p, "cy", "shape.flare")?);
|
||||
let pct = |k: &str, d: f64| (f64_or(p, k, d) / 100.0).clamp(0.0, 10.0);
|
||||
let r = (f64_or(p, "diameter", 100.0) / 2.0).max(0.5);
|
||||
let opacity = pct("opacity", 50.0).min(1.0) as f32;
|
||||
let brightness = pct("brightness", 30.0).min(1.0) as f32;
|
||||
let growth = pct("growth", 20.0);
|
||||
let fuzz = pct("fuzziness", 50.0).min(1.0);
|
||||
let rays = p.get("rays").and_then(Value::as_u64).unwrap_or(15).min(50) as usize;
|
||||
let longest = pct("longest", 300.0);
|
||||
let ray_fuzz = pct("rayFuzziness", 100.0).min(1.0);
|
||||
let rings = p.get("rings").and_then(Value::as_u64).unwrap_or(10).min(50) as usize;
|
||||
let largest = pct("largest", 50.0);
|
||||
let end = match (p.get("x2").and_then(Value::as_f64), p.get("y2").and_then(Value::as_f64)) {
|
||||
(Some(x), Some(y)) => Point::new(x, y),
|
||||
_ => {
|
||||
let a = f64_or(p, "direction", 45.0).to_radians();
|
||||
c + vectorcraft_geom::Vec2::new(a.cos(), -a.sin()) * f64_or(p, "pathLength", 300.0)
|
||||
}
|
||||
};
|
||||
// Deterministic pseudo-random sizes (same parameters → same flare).
|
||||
let mut seed = p.get("seed").and_then(Value::as_u64).unwrap_or(0x2545_f491_4f6c_dd1d) | 1;
|
||||
let mut rnd = move || {
|
||||
seed ^= seed << 13;
|
||||
seed ^= seed >> 7;
|
||||
seed ^= seed << 17;
|
||||
(seed >> 11) as f64 / (1u64 << 53) as f64
|
||||
};
|
||||
let stop = |offset: f32, opacity: f32| GradientStop { offset, color: vectorcraft_color::Color::WHITE, opacity, midpoint: 0.5 };
|
||||
let radial = |centre: Point, radius: f64, stops: Vec<GradientStop>| {
|
||||
let mut g = GradientPaint::new(Gradient { kind: GradientKind::Radial, stops });
|
||||
g.geom = Some(GradientGeom { start: centre, end: centre + vectorcraft_geom::Vec2::new(radius, 0.0), aspect: 1.0 });
|
||||
Paint::Gradient(Box::new(g))
|
||||
};
|
||||
let circle = |centre: Point, radius: f64| shapes::ellipse(Rect::from_center_size(centre, (radius * 2.0, radius * 2.0)));
|
||||
let mut parts: Vec<(&str, PathData, Paint, f32)> = vec![];
|
||||
// Halo: a soft ring just outside the centre.
|
||||
let halo_r = r * (1.0 + growth) * 1.6;
|
||||
let inner = (1.0 - fuzz as f32 * 0.6).clamp(0.05, 0.95);
|
||||
parts.push(("Halo", circle(c, halo_r), radial(c, halo_r, vec![stop(0.0, 0.0), stop(inner * 0.75, 0.0), stop(inner, 0.5), stop(1.0, 0.0)]), 1.0));
|
||||
// Rays: thin spikes of random length (Longest = % of the centre radius), fading outwards.
|
||||
if rays > 0 {
|
||||
let mut bp = vectorcraft_geom::BezPath::new();
|
||||
let mut reach: f64 = 0.0;
|
||||
for i in 0..rays {
|
||||
let a = std::f64::consts::TAU * (i as f64 + rnd() * 0.6) / rays as f64;
|
||||
let len = r * longest * (1.0 - ray_fuzz * 0.75 * rnd()).max(0.1);
|
||||
reach = reach.max(len);
|
||||
let w = r * 0.035;
|
||||
let dir = vectorcraft_geom::Vec2::new(a.cos(), a.sin());
|
||||
let n = vectorcraft_geom::Vec2::new(-dir.y, dir.x) * w;
|
||||
bp.move_to(c + n);
|
||||
bp.line_to(c + dir * len);
|
||||
bp.line_to(c - n);
|
||||
bp.line_to(c - dir * (w * 4.0));
|
||||
bp.close_path();
|
||||
}
|
||||
parts.push(("Rays", PathData::from_bezpath(&bp), radial(c, reach, vec![stop(0.0, 0.9), stop(0.25, 0.35), stop(1.0, 0.0)]), 1.0));
|
||||
}
|
||||
// Centre: bright core fading to transparent.
|
||||
parts.push(("Center", circle(c, r), radial(c, r, vec![stop(0.0, 1.0), stop(0.25 + brightness * 0.5, 0.6), stop(1.0, 0.0)]), opacity));
|
||||
// Rings along the path to the end point (Largest = % of the average ring size).
|
||||
for _ in 0..rings {
|
||||
let t = rnd();
|
||||
let at = c + (end - c) * t;
|
||||
let rr = (r * largest * (0.25 + rnd() * 0.75)).max(0.5);
|
||||
let solid = rnd() < 0.5;
|
||||
let stops = if solid {
|
||||
vec![stop(0.0, 0.25), stop(0.8, 0.2), stop(1.0, 0.0)]
|
||||
} else {
|
||||
vec![stop(0.0, 0.0), stop(0.7, 0.05), stop(0.92, 0.35), stop(1.0, 0.0)]
|
||||
};
|
||||
parts.push(("Ring", circle(at, rr), radial(at, rr, stops), 1.0));
|
||||
}
|
||||
let parent = s.doc()?.insertion_parent();
|
||||
let id = s.edit("Flare", |d, sel| {
|
||||
let children = parts
|
||||
.into_iter()
|
||||
.map(|(name, pd, paint, op)| {
|
||||
let mut n = Node::path(d.alloc_id(), pd, Appearance::basic(paint, Paint::None, 0.0));
|
||||
n.name = Some(name.into());
|
||||
n.blend = BlendMode::Screen;
|
||||
n.opacity = op;
|
||||
std::sync::Arc::new(n)
|
||||
})
|
||||
.collect();
|
||||
let gid = d.alloc_id();
|
||||
let mut g = Node::group(gid, children);
|
||||
g.name = Some("Flare".into());
|
||||
d.insert(parent, usize::MAX, g)?;
|
||||
sel.set([gid]);
|
||||
Ok(gid)
|
||||
})?;
|
||||
Ok(json!({ "id": id.0 }))
|
||||
}
|
||||
|
||||
fn line(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let a = Point::new(f64_req(p, "x1", "shape.line")?, f64_req(p, "y1", "shape.line")?);
|
||||
let b = Point::new(f64_req(p, "x2", "shape.line")?, f64_req(p, "y2", "shape.line")?);
|
||||
|
||||
@@ -86,6 +86,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
has_selection,
|
||||
delete_anchors
|
||||
),
|
||||
cmd!(
|
||||
"path.reshape",
|
||||
"Reshape",
|
||||
[],
|
||||
None,
|
||||
"{id, x, y, dx, dy, tol=3, radius?} Reshape tool: grab the path at (x, y) (adding an anchor there unless one is within tol) and drag it by (dx, dy); nearby anchors follow with a smooth falloff over radius (default: half the subpath's size)",
|
||||
has_doc,
|
||||
reshape
|
||||
),
|
||||
cmd!("path.insertAnchor", "Add Anchor Point", [], None, "{id, subpath, segment, t: 0..1}", has_doc, insert_anchor),
|
||||
cmd!("path.cutAtAnchors", "Cut Path at Selected Anchor Points", [], None, "{}", has_selection, cut_at_anchors),
|
||||
]
|
||||
@@ -416,6 +425,44 @@ pub(crate) fn delete_anchors(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
ok()
|
||||
}
|
||||
|
||||
fn reshape(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let id = id_param(p, "id").ok_or_else(|| bad("path.reshape", "missing id"))?;
|
||||
let at = Point::new(f64_req(p, "x", "path.reshape")?, f64_req(p, "y", "path.reshape")?);
|
||||
let delta = Vec2::new(f64_or(p, "dx", 0.0), f64_or(p, "dy", 0.0));
|
||||
let tol = f64_or(p, "tol", 3.0).max(0.0);
|
||||
let radius = p.get("radius").and_then(Value::as_f64);
|
||||
s.edit("Reshape", |d, _| {
|
||||
let path = path_mut(d, id)?;
|
||||
let (si, seg, t, _, _) = path.nearest(at).ok_or_else(|| EngineError::Other("empty path".into()))?;
|
||||
let sp = &mut path.subpaths[si];
|
||||
let grabbed = match sp.anchors.iter().enumerate().map(|(i, a)| (i, a.p.distance(at))).min_by(|a, b| a.1.total_cmp(&b.1)) {
|
||||
Some((i, dist)) if dist <= tol => i,
|
||||
_ => sp.insert_anchor(seg, t),
|
||||
};
|
||||
let origin = sp.anchors[grabbed].p;
|
||||
let r = radius.unwrap_or_else(|| {
|
||||
let mut bp = vectorcraft_geom::BezPath::new();
|
||||
sp.to_bezpath_into(&mut bp);
|
||||
let b = vectorcraft_geom::Shape::bounding_box(&bp);
|
||||
(b.width().hypot(b.height()) / 2.0).max(1.0)
|
||||
});
|
||||
for (i, a) in sp.anchors.iter_mut().enumerate() {
|
||||
let w = if i == grabbed {
|
||||
1.0
|
||||
} else {
|
||||
let q = (a.p.distance(origin) / r).min(1.0);
|
||||
(1.0 - q * q).powi(2)
|
||||
};
|
||||
let v = delta * w;
|
||||
a.p += v;
|
||||
a.h_in += v;
|
||||
a.h_out += v;
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
ok()
|
||||
}
|
||||
|
||||
fn insert_anchor(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let id = id_param(p, "id").ok_or_else(|| bad("path.insertAnchor", "missing id"))?;
|
||||
let si = p.get("subpath").and_then(Value::as_u64).unwrap_or(0) as usize;
|
||||
|
||||
@@ -310,3 +310,45 @@ fn every_command_survives_empty_params() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flare_tool_two_step_gesture_is_one_undo() {
|
||||
let mut s = session();
|
||||
let v = ViewInfo::default();
|
||||
let undo_before = s.doc().unwrap().history.undo.len();
|
||||
s.select_tool("flare", v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Down, 200.0, 200.0), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Drag, 240.0, 200.0), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Up, 240.0, 200.0), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Move, 500.0, 400.0), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Down, 500.0, 400.0), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Up, 500.0, 400.0), v).unwrap();
|
||||
let st = s.doc().unwrap();
|
||||
assert_eq!(st.history.undo.len(), undo_before + 1);
|
||||
let g = st.doc.node(st.selection.objects[0]).unwrap();
|
||||
assert_eq!(g.name.as_deref(), Some("Flare"));
|
||||
// Halo + rays + centre + 10 rings; the rings reach towards the end point.
|
||||
let NodeKind::Group { children, .. } = &g.kind else { panic!("flare is a group") };
|
||||
assert_eq!(children.len(), 13);
|
||||
let b = g.geometric_bounds().unwrap();
|
||||
assert!(b.x1 > 400.0 && b.y1 > 300.0, "{b:?}");
|
||||
let centre = children.iter().find(|c| c.name.as_deref() == Some("Center")).unwrap().geometric_bounds().unwrap();
|
||||
assert!((centre.width() - 80.0).abs() < 0.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reshape_moves_the_grabbed_point_and_its_neighbourhood() {
|
||||
let mut s = session();
|
||||
let r = rect(&mut s, 0.0, 0.0, 100.0, 100.0);
|
||||
// Grab the middle of the top edge: a new anchor appears there and moves the full delta.
|
||||
s.execute("path.reshape", &json!({"id": r.0, "x": 50.0, "y": 0.0, "dx": 0.0, "dy": -40.0})).unwrap();
|
||||
let st = s.doc().unwrap();
|
||||
let pd = st.doc.node(r).unwrap().path_data().unwrap().clone();
|
||||
assert_eq!(pd.anchor_count(), 5);
|
||||
let pts: Vec<_> = pd.anchors().map(|(_, _, a)| a.p).collect();
|
||||
assert!(pts.iter().any(|p| (p.x - 50.0).abs() < 1e-6 && (p.y + 40.0).abs() < 1e-6), "{pts:?}");
|
||||
// Top corners follow partially, bottom corners (far away) stay put.
|
||||
let tl = pts.iter().find(|p| p.x < 1.0 && p.y < 0.0).expect("top-left moved up");
|
||||
assert!(tl.y > -40.0);
|
||||
assert!(pts.iter().filter(|p| (p.y - 100.0).abs() < 1e-9).count() == 2);
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
//! Flare and Reshape tools.
|
||||
//!
|
||||
//! Flare: drag sets the centre and its size (click without dragging opens the Flare Tool Options),
|
||||
//! then a click sets the end point of the rings. Reshape: drag a point of a selected path; the
|
||||
//! engine's `path.reshape` adds an anchor there if needed and moves the neighbourhood smoothly.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_geom::{Point, Vec2};
|
||||
|
||||
use crate::distort::{BLUE, ellipse_path};
|
||||
use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey};
|
||||
|
||||
pub fn create(id: &str) -> Option<Box<dyn Tool>> {
|
||||
Some(match id {
|
||||
"flare" => Box::new(FlareTool::default()),
|
||||
"reshape" => Box::new(ReshapeTool::default()),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Flare Tool: centre drag, then a click for the rings' end point.
|
||||
#[derive(Default)]
|
||||
pub struct FlareTool {
|
||||
/// Pressed at (centre) and the current drag point.
|
||||
drag: Option<(Point, Point)>,
|
||||
/// Centre placed, waiting for the end-point click: (centre, diameter).
|
||||
placed: Option<(Point, f64)>,
|
||||
hover: Point,
|
||||
began: bool,
|
||||
}
|
||||
|
||||
impl FlareTool {
|
||||
fn params(c: Point, diameter: f64, end: Option<Point>) -> Value {
|
||||
let mut v = json!({ "cx": c.x, "cy": c.y, "diameter": diameter.max(1.0) });
|
||||
if let Some(e) = end {
|
||||
v["x2"] = json!(e.x);
|
||||
v["y2"] = json!(e.y);
|
||||
}
|
||||
v
|
||||
}
|
||||
}
|
||||
|
||||
impl Tool for FlareTool {
|
||||
fn id(&self) -> &'static str {
|
||||
"flare"
|
||||
}
|
||||
fn busy(&self) -> bool {
|
||||
self.drag.is_some() || self.placed.is_some()
|
||||
}
|
||||
fn pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec<Action> {
|
||||
self.hover = ev.pos;
|
||||
match ev.kind {
|
||||
PointerKind::Down => {
|
||||
if self.placed.is_none() {
|
||||
self.drag = Some((ev.pos, ev.pos));
|
||||
}
|
||||
vec![]
|
||||
}
|
||||
PointerKind::Drag => {
|
||||
if let Some((c, _)) = self.drag {
|
||||
self.drag = Some((c, ev.pos));
|
||||
if !self.began && ev.pos.distance(c) >= cx.tol(3.0) {
|
||||
self.began = true;
|
||||
return vec![
|
||||
Action::Begin("Flare".into()),
|
||||
Action::Preview("shape.flare".into(), Self::params(c, 2.0 * ev.pos.distance(c), None)),
|
||||
];
|
||||
}
|
||||
if self.began {
|
||||
return vec![Action::Preview("shape.flare".into(), Self::params(c, 2.0 * ev.pos.distance(c), None))];
|
||||
}
|
||||
} else if let Some((c, d)) = self.placed {
|
||||
return vec![Action::Preview("shape.flare".into(), Self::params(c, d, Some(ev.pos)))];
|
||||
}
|
||||
vec![]
|
||||
}
|
||||
PointerKind::Up => {
|
||||
if let Some((c, d)) = self.placed.take() {
|
||||
self.began = false;
|
||||
return vec![Action::Preview("shape.flare".into(), Self::params(c, d, Some(ev.pos))), Action::Commit];
|
||||
}
|
||||
let Some((c, p)) = self.drag.take() else { return vec![] };
|
||||
if self.began {
|
||||
// Keep the interaction open: the next click places the end point.
|
||||
self.placed = Some((c, 2.0 * p.distance(c)));
|
||||
vec![]
|
||||
} else {
|
||||
vec![Action::Dialog("flare".into(), json!({ "x": c.x, "y": c.y }))]
|
||||
}
|
||||
}
|
||||
PointerKind::Move => {
|
||||
if let Some((c, d)) = self.placed {
|
||||
return vec![Action::Preview("shape.flare".into(), Self::params(c, d, Some(ev.pos)))];
|
||||
}
|
||||
vec![]
|
||||
}
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
fn key(&mut self, _cx: &ToolContext, key: ToolKey, _mods: Mods) -> Vec<Action> {
|
||||
if key == ToolKey::Escape && self.busy() {
|
||||
self.drag = None;
|
||||
self.placed = None;
|
||||
let began = std::mem::take(&mut self.began);
|
||||
return if began { vec![Action::Cancel] } else { vec![] };
|
||||
}
|
||||
vec![]
|
||||
}
|
||||
fn deactivate(&mut self, _cx: &ToolContext) -> Vec<Action> {
|
||||
// Switching away after the centre drag keeps the flare with its default rings.
|
||||
self.drag = None;
|
||||
if self.placed.take().is_some() && std::mem::take(&mut self.began) { vec![Action::Commit] } else { vec![] }
|
||||
}
|
||||
fn overlays(&self, _cx: &ToolContext) -> Vec<Overlay> {
|
||||
match (self.drag, self.placed) {
|
||||
(Some((c, p)), _) => {
|
||||
vec![Overlay::Path { path: ellipse_path(c, p.distance(c), p.distance(c), 0.0), color: BLUE, width: 1.0, dashed: false }]
|
||||
}
|
||||
(_, Some((c, _))) => vec![Overlay::Line { a: c, b: self.hover, color: BLUE, dashed: true }],
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
fn cursor(&self, _cx: &ToolContext, _p: Point, _mods: Mods) -> Cursor {
|
||||
Cursor::Crosshair
|
||||
}
|
||||
}
|
||||
|
||||
/// Reshape Tool: drag a point on a selected path.
|
||||
#[derive(Default)]
|
||||
pub struct ReshapeTool {
|
||||
/// (path, grab point) while dragging.
|
||||
grab: Option<(vectorcraft_doc::NodeId, Point)>,
|
||||
began: bool,
|
||||
}
|
||||
|
||||
impl ReshapeTool {
|
||||
/// The selected path under `p` (Reshape works on selected paths only, like Illustrator).
|
||||
fn hit(cx: &ToolContext, p: Point) -> Option<(vectorcraft_doc::NodeId, Point)> {
|
||||
let tol = cx.tol(4.0);
|
||||
cx.selection.objects.iter().copied().filter(|id| cx.doc.is_editable(*id)).find_map(|id| {
|
||||
let pd = cx.doc.node(id)?.path_data()?;
|
||||
let (_, _, _, q, d) = pd.nearest(p)?;
|
||||
(d <= tol).then_some((id, q))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Tool for ReshapeTool {
|
||||
fn id(&self) -> &'static str {
|
||||
"reshape"
|
||||
}
|
||||
fn busy(&self) -> bool {
|
||||
self.grab.is_some()
|
||||
}
|
||||
fn pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec<Action> {
|
||||
match ev.kind {
|
||||
PointerKind::Down => {
|
||||
self.grab = Self::hit(cx, ev.pos);
|
||||
self.began = false;
|
||||
vec![]
|
||||
}
|
||||
PointerKind::Drag => {
|
||||
let Some((id, at)) = self.grab else { return vec![] };
|
||||
let mut d: Vec2 = ev.pos - at;
|
||||
if ev.mods.shift {
|
||||
d = vectorcraft_geom::constrain_angle(d, 45.0);
|
||||
}
|
||||
let mut out = vec![];
|
||||
if !self.began {
|
||||
if d.hypot() < cx.tol(2.0) {
|
||||
return out;
|
||||
}
|
||||
self.began = true;
|
||||
out.push(Action::Begin("Reshape".into()));
|
||||
}
|
||||
out.push(Action::Preview(
|
||||
"path.reshape".into(),
|
||||
json!({ "id": id.0, "x": at.x, "y": at.y, "dx": d.x, "dy": d.y, "tol": cx.tol(4.0) }),
|
||||
));
|
||||
out
|
||||
}
|
||||
PointerKind::Up => {
|
||||
self.grab = None;
|
||||
if std::mem::take(&mut self.began) { vec![Action::Commit] } else { vec![] }
|
||||
}
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
fn key(&mut self, _cx: &ToolContext, key: ToolKey, _mods: Mods) -> Vec<Action> {
|
||||
if key == ToolKey::Escape && self.grab.take().is_some() && std::mem::take(&mut self.began) {
|
||||
return vec![Action::Cancel];
|
||||
}
|
||||
vec![]
|
||||
}
|
||||
fn cursor(&self, cx: &ToolContext, p: Point, _mods: Mods) -> Cursor {
|
||||
if self.grab.is_some() || Self::hit(cx, p).is_some() { Cursor::Move } else { Cursor::Arrow }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn create_covers_both() {
|
||||
assert_eq!(create("flare").unwrap().id(), "flare");
|
||||
assert_eq!(create("reshape").unwrap().id(), "reshape");
|
||||
assert!(create("pen").is_none());
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ pub mod catalog;
|
||||
pub mod direct;
|
||||
pub mod distort;
|
||||
pub mod draw2;
|
||||
pub mod extra;
|
||||
pub mod guides;
|
||||
pub mod meshblend;
|
||||
pub mod pen;
|
||||
@@ -257,6 +258,7 @@ pub fn create(id: &str) -> Box<dyn Tool> {
|
||||
.or_else(|| xform::create(other))
|
||||
.or_else(|| meshblend::create(other))
|
||||
.or_else(|| distort::create(other))
|
||||
.or_else(|| extra::create(other))
|
||||
.unwrap_or_else(|| Box::new(NoopTool(tool_info(other).map(|t| t.id).unwrap_or("selection")))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,12 @@ pub fn open_tool_dialog(app: &mut VectorcraftApp, kind: &str, p: Value) {
|
||||
"polygon" => Dialog::new(kind, json!({"x": x, "y": y, "radius": "50 pt", "sides": 6})),
|
||||
"star" => Dialog::new(kind, json!({"x": x, "y": y, "radius1": "50 pt", "radius2": "25 pt", "points": 5})),
|
||||
"lineSegment" => Dialog::new(kind, json!({"x": x, "y": y, "length": "100 pt", "angle": 0})),
|
||||
// Flare Tool Options (Center / Halo / Rays / Rings), applied through the generic command dialog.
|
||||
"flare" => Dialog::new(
|
||||
"command",
|
||||
json!({"__command": "shape.flare", "__label": "Flare Tool Options", "cx": x, "cy": y, "diameter": 100, "opacity": 50, "brightness": 30,
|
||||
"growth": 20, "fuzziness": 50, "rays": 15, "longest": 300, "rayFuzziness": 100, "pathLength": 300, "rings": 10, "largest": 50, "direction": 45}),
|
||||
),
|
||||
"rotate" | "reflect" | "scale" | "shear" | "artboardOptions" => {
|
||||
let mut base = match kind {
|
||||
"rotate" => json!({"angle": 0}),
|
||||
|
||||
Reference in new issue
Block a user