mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
G11: Arc segment dialog options wired to shape.arc
Replace length/angle with Length X/Y, reference point, Type, Base Along, Slope and Fill Arc; extend geom and shape.arc params. Co-authored-by: nrq6993-a11y <nrq6993-a11y@users.noreply.github.com>
This commit is contained in:
committed by
bflatastic
co-authored by
nrq6993-a11y
parent
d6e64cb080
commit
5359c92a19
@@ -32,7 +32,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
),
|
||||
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),
|
||||
cmd!(
|
||||
"shape.arc",
|
||||
"Arc",
|
||||
[],
|
||||
None,
|
||||
"{x1, y1, x2, y2, closed?, slope?} or {x, y, lengthX, lengthY, reference?, baseAlong?: x|y, slope?, closed?, fillArc?} → {id}",
|
||||
has_doc,
|
||||
arc
|
||||
),
|
||||
cmd!(
|
||||
"shape.rectangularGrid",
|
||||
"Rectangular Grid",
|
||||
@@ -393,9 +401,34 @@ fn spiral(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
|
||||
fn arc(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let a = Point::new(f64_req(p, "x1", "shape.arc")?, f64_req(p, "y1", "shape.arc")?);
|
||||
let b = Point::new(f64_req(p, "x2", "shape.arc")?, f64_req(p, "y2", "shape.arc")?);
|
||||
add_art(s, "Arc", path_kind(shapes::arc(a, b, 0.0, switch_or(p, "closed", false, "shape.arc")?), None), None)
|
||||
let closed = switch_or(p, "closed", false, "shape.arc")?;
|
||||
let fill_arc = switch_or(p, "fillArc", false, "shape.arc")?;
|
||||
let base_x = !matches!(p.get("baseAlong").and_then(Value::as_str), Some("y" | "Y"));
|
||||
let path = if p.get("lengthX").is_some() || p.get("lengthY").is_some() {
|
||||
let anchor = Point::new(f64_req(p, "x", "shape.arc")?, f64_req(p, "y", "shape.arc")?);
|
||||
let reference = p.get("reference").and_then(Value::as_u64).unwrap_or(0) as usize;
|
||||
shapes::arc_segment(
|
||||
anchor,
|
||||
reference,
|
||||
number_or(p, "lengthX", 100.0, "shape.arc")?,
|
||||
number_or(p, "lengthY", 100.0, "shape.arc")?,
|
||||
base_x,
|
||||
number_or(p, "slope", 50.0, "shape.arc")?,
|
||||
closed,
|
||||
)
|
||||
} else {
|
||||
let a = Point::new(f64_req(p, "x1", "shape.arc")?, f64_req(p, "y1", "shape.arc")?);
|
||||
let b = Point::new(f64_req(p, "x2", "shape.arc")?, f64_req(p, "y2", "shape.arc")?);
|
||||
let slope = number_or(p, "slope", 100.0, "shape.arc")?;
|
||||
shapes::arc(a, b, slope, closed, base_x)
|
||||
};
|
||||
let fill = if fill_arc { s.paint.fill.clone() } else { vectorcraft_color::Paint::None };
|
||||
let look = s.new_art_look(
|
||||
fill,
|
||||
if s.paint.stroke.is_none() { vectorcraft_color::Paint::solid(vectorcraft_color::Color::BLACK) } else { s.paint.stroke.clone() },
|
||||
s.paint.stroke_width.max(0.1),
|
||||
);
|
||||
add_look(s, "Arc", path_kind(path, None), look, None)
|
||||
}
|
||||
|
||||
/// The grid `paths` (bottom first) as one selected group in the new-art stroke, filled with the
|
||||
|
||||
+28
-10
@@ -176,17 +176,33 @@ pub fn line(a: Point, b: Point) -> PathData {
|
||||
PathData::single(SubPath::polyline(&[a, b], false))
|
||||
}
|
||||
|
||||
/// Quarter-ellipse arc from `a` to `b` (Arc tool default: concave, open, x-axis base).
|
||||
pub fn arc(a: Point, b: Point, slope: f64, closed: bool) -> PathData {
|
||||
// Corner of the arc's bounding box is at (a.x, b.y): the arc bows away from it.
|
||||
let k = KAPPA * (1.0 + slope.clamp(-1.0, 1.0) * 0.0);
|
||||
let corner = Point::new(b.x, a.y);
|
||||
let h1 = a + (corner - a) * k;
|
||||
let h2 = b + (corner - b) * k;
|
||||
/// Bounding box of an arc segment when reference point `reference` (0…8) is placed on `anchor`.
|
||||
pub fn arc_bbox(anchor: Point, reference: usize, length_x: f64, length_y: f64) -> Rect {
|
||||
let w = length_x.abs().max(1e-9);
|
||||
let h = length_y.abs().max(1e-9);
|
||||
let rp = crate::reference_point(Rect::new(0.0, 0.0, w, h), reference.min(8));
|
||||
Rect::new(anchor.x - rp.x, anchor.y - rp.y, anchor.x - rp.x + w, anchor.y - rp.y + h)
|
||||
}
|
||||
|
||||
/// Arc segment from the Arc tool dialog: lengths, reference point, base axis, slope and open/closed.
|
||||
pub fn arc_segment(anchor: Point, reference: usize, length_x: f64, length_y: f64, base_along_x: bool, slope: f64, closed: bool) -> PathData {
|
||||
let r = arc_bbox(anchor, reference, length_x, length_y).abs();
|
||||
let (a, b) = if base_along_x { (Point::new(r.x0, r.y0), Point::new(r.x1, r.y1)) } else { (Point::new(r.x0, r.y1), Point::new(r.x1, r.y0)) };
|
||||
arc(a, b, slope, closed, base_along_x)
|
||||
}
|
||||
|
||||
/// Quarter-ellipse arc from `a` to `b` (Arc tool default: open, x-axis base when `base_along_x`).
|
||||
/// `slope` is −100…100 (concave…convex); 0 is a straight chord.
|
||||
pub fn arc(a: Point, b: Point, slope: f64, closed: bool, base_along_x: bool) -> PathData {
|
||||
let t = slope.clamp(-100.0, 100.0) / 100.0;
|
||||
let corner = if t >= 0.0 { Point::new(b.x, a.y) } else { Point::new(a.x, b.y) };
|
||||
let k = KAPPA * t.abs();
|
||||
let h1 = if k == 0.0 { a } else { a + (corner - a) * k };
|
||||
let h2 = if k == 0.0 { b } else { b + (corner - b) * k };
|
||||
let mut anchors =
|
||||
vec![Anchor { p: a, h_in: a, h_out: h1, kind: AnchorKind::Corner }, Anchor { p: b, h_in: h2, h_out: b, kind: AnchorKind::Corner }];
|
||||
if closed {
|
||||
let o = Point::new(a.x, b.y);
|
||||
let o = if base_along_x { Point::new(a.x, b.y) } else { Point::new(b.x, a.y) };
|
||||
anchors.push(Anchor::corner(o));
|
||||
}
|
||||
PathData::single(SubPath::new(anchors, closed))
|
||||
@@ -531,7 +547,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn arc_open_and_closed() {
|
||||
assert_eq!(arc(Point::ZERO, Point::new(10.0, 10.0), 0.0, false).anchor_count(), 2);
|
||||
assert!(arc(Point::ZERO, Point::new(10.0, 10.0), 0.0, true).subpaths[0].closed);
|
||||
assert_eq!(arc(Point::ZERO, Point::new(10.0, 10.0), 0.0, false, true).anchor_count(), 2);
|
||||
assert!(arc(Point::ZERO, Point::new(10.0, 10.0), 0.0, true, true).subpaths[0].closed);
|
||||
let seg = arc_segment(Point::ZERO, 0, 100.0, 50.0, true, 50.0, false);
|
||||
assert_eq!(seg.anchor_count(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ const ROWS: &[Row] = &[
|
||||
(SYMBOLISM, Some("symbolism"), &["diameter", "intensity", "density"]),
|
||||
(&["mirrorCut"], None, &["axis", "keep"]),
|
||||
(&["puppetWarp"], None, &["showMesh", "expand"]),
|
||||
(&["arc"], None, &["closed"]),
|
||||
(&["arc"], None, &["closed", "lengthX", "lengthY", "reference", "baseAlong", "slope", "fillArc"]),
|
||||
(&["spiral"], None, &["decay", "segments", "clockwise"]),
|
||||
(&["rectangularGrid"], None, &["rows", "columns", "rowSkew", "columnSkew", "frame", "fill"]),
|
||||
(&["polarGrid"], None, &["concentric", "radial", "concentricSkew", "radialSkew", "compound", "fill"]),
|
||||
|
||||
@@ -63,7 +63,8 @@ fn lengths(kind: &str) -> &'static [&'static str] {
|
||||
"roundedRectangle" => &["width", "height", "radius"],
|
||||
"polygon" => &["radius"],
|
||||
"star" => &["radius1", "radius2"],
|
||||
"lineSegment" | "arc" => &["length"],
|
||||
"lineSegment" => &["length"],
|
||||
"arc" => &["lengthX", "lengthY"],
|
||||
"offsetPath" => &["offset"],
|
||||
_ => &[],
|
||||
}
|
||||
@@ -73,7 +74,7 @@ fn lengths(kind: &str) -> &'static [&'static str] {
|
||||
/// number: fields that ↑/↓ step by one (Shift: ten) and that do math, as the distances do.
|
||||
fn suffix(kind: &str, key: &str) -> Option<&'static str> {
|
||||
match (kind, key) {
|
||||
("rotate" | "shear" | "reflect" | "move" | "arc" | "lineSegment", "angle") => Some("°"),
|
||||
("rotate" | "shear" | "reflect" | "move" | "lineSegment", "angle") => Some("°"),
|
||||
("scale", "sx" | "sy") => Some("%"),
|
||||
_ => None,
|
||||
}
|
||||
@@ -96,7 +97,7 @@ fn order(kind: &str) -> &'static [&'static str] {
|
||||
"rectangularGrid" => &["width", "height", "rows", "rowSkew", "columns", "columnSkew"],
|
||||
"polarGrid" => &["width", "height", "concentric", "concentricSkew", "radial", "radialSkew"],
|
||||
"artboardOptions" => &["name", "width", "height"],
|
||||
"arc" | "lineSegment" => &["length", "angle"],
|
||||
"lineSegment" => &["length", "angle"],
|
||||
_ => &[],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
//! Star, Line Segment and Arc tool options, and the two grids (size, divider counts and skews, switches).
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::Unit;
|
||||
use vectorcraft_geom::{Point, Rect};
|
||||
|
||||
use super::{DialogSpec, form, run_and_close};
|
||||
@@ -21,7 +22,7 @@ fn title(kind: &str) -> &'static str {
|
||||
"star" => tl!("Star"),
|
||||
"rectangularGrid" => tl!("Rectangular Grid"),
|
||||
"polarGrid" => tl!("Polar Grid"),
|
||||
"arc" => tl!("Arc"),
|
||||
"arc" => tl!("Arc Segment Tool Options"),
|
||||
_ => tl!("Line Segment Tool Options"),
|
||||
}
|
||||
}
|
||||
@@ -49,6 +50,21 @@ fn set_grid_placed(d: &mut Dialog, r: Rect) {
|
||||
d.fields.insert("height".into(), json!(r.height()));
|
||||
}
|
||||
|
||||
/// Arc segment dialog: keep reference `refi` on the canvas click (`__anchorX`/`__anchorY`).
|
||||
pub(crate) fn arc_pin_to_anchor(d: &mut Dialog) {
|
||||
let anchor = Point::new(d.f64("__anchorX", d.f64("x", 0.0)), d.f64("__anchorY", d.f64("y", 0.0)));
|
||||
let refi = d.fields.get("reference").and_then(|v| v.as_u64()).map(|n| (n as usize).min(8)).unwrap_or(0);
|
||||
let w = d.f64("lengthX", 100.0).abs().max(1e-9);
|
||||
let h = d.f64("lengthY", 100.0).abs().max(1e-9);
|
||||
let rp = vectorcraft_geom::reference_point(Rect::new(0.0, 0.0, w, h), refi);
|
||||
d.fields.insert("x".into(), json!(anchor.x - rp.x));
|
||||
d.fields.insert("y".into(), json!(anchor.y - rp.y));
|
||||
}
|
||||
|
||||
fn arc_reference(d: &Dialog) -> usize {
|
||||
d.fields.get("reference").and_then(|v| v.as_u64()).map(|n| (n as usize).min(8)).unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Top-left `x`/`y` so reference point `refi` sits on the canvas click (`__anchorX`/`__anchorY`).
|
||||
pub(crate) fn grid_pin_to_anchor(d: &mut Dialog) {
|
||||
let anchor = Point::new(d.f64("__anchorX", d.f64("x", 0.0)), d.f64("__anchorY", d.f64("y", 0.0)));
|
||||
@@ -79,12 +95,10 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
set_grid_placed(d, artboard_rect(was, refi, None, None, Some(w), Some(h), false));
|
||||
}
|
||||
});
|
||||
} else if d.kind == "arc" {
|
||||
arc_form(ui, d, unit);
|
||||
} else {
|
||||
form::grid(ui, d, unit);
|
||||
if d.kind == "arc" {
|
||||
ui.add_space(8.0);
|
||||
form::check(ui, d, "closed", tl!("Closed"));
|
||||
}
|
||||
}
|
||||
let own = match d.kind.as_str() {
|
||||
"rectangularGrid" => ("frame", tl!("Use Outside Rectangle As Frame")),
|
||||
@@ -98,6 +112,50 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn arc_form(ui: &mut egui::Ui, d: &mut Dialog, unit: Unit) {
|
||||
ui.horizontal(|ui| {
|
||||
let refi = arc_reference(d);
|
||||
if let Some(i) = widgets::reference_point(ui, refi) {
|
||||
d.fields.insert("reference".into(), json!(i));
|
||||
arc_pin_to_anchor(d);
|
||||
}
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
let rail = crate::theme::Tokens::get(ui.ctx()).input_border;
|
||||
egui::Grid::new("arc-dlg").num_columns(2).spacing([10.0, 8.0]).show(ui, |ui| {
|
||||
let lx0 = d.f64("lengthX", 100.0);
|
||||
form::length_field(ui, d, "lengthX", tl!("Length X-Axis:"), unit);
|
||||
if d.f64("lengthX", 100.0) != lx0 {
|
||||
arc_pin_to_anchor(d);
|
||||
}
|
||||
let ly0 = d.f64("lengthY", 100.0);
|
||||
form::length_field(ui, d, "lengthY", tl!("Length Y-Axis:"), unit);
|
||||
if d.f64("lengthY", 100.0) != ly0 {
|
||||
arc_pin_to_anchor(d);
|
||||
}
|
||||
form::choice(ui, d, "arcType", tl!("Type:"), (form::FIELD_W, form::FIELD_W - 12.0), &[("open", tl!("Open")), ("closed", tl!("Closed"))]);
|
||||
form::choice(
|
||||
ui,
|
||||
d,
|
||||
"baseAlong",
|
||||
tl!("Base Along:"),
|
||||
(form::FIELD_W, form::FIELD_W - 12.0),
|
||||
&[
|
||||
("x", crate::i18n::tr_ctx(crate::i18n::current(), "axis", "X Axis")),
|
||||
("y", crate::i18n::tr_ctx(crate::i18n::current(), "axis", "Y Axis")),
|
||||
],
|
||||
);
|
||||
form::row_label(ui, tl!("Slope:"));
|
||||
ui.vertical(|ui| {
|
||||
form::slider_field_named(ui, d, "slope", tl!("Slope:"), -100.0..=100.0, "", &|_| rail);
|
||||
form::slider_ends(ui, form::SLIDER_LABEL, (tl!("Concave"), tl!("Convex")));
|
||||
});
|
||||
ui.end_row();
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
form::check(ui, d, "fillArc", tl!("Fill Arc"));
|
||||
}
|
||||
|
||||
/// The top-left corner of a rectangle or ellipse: the click itself, or with Alt held (draw from centre) the click minus half the size.
|
||||
fn box_origin(d: &Dialog) -> (f64, f64) {
|
||||
let (x, y) = (d.f64("x", 0.0), d.f64("y", 0.0));
|
||||
@@ -120,7 +178,19 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
values[key] = json!(d.f64(key, default).clamp(min, 1000.0) as u64);
|
||||
values
|
||||
})
|
||||
.or_else(|| (d.kind == "arc").then(|| json!({"closed": d.bool("closed")})));
|
||||
.or_else(|| {
|
||||
(d.kind == "arc").then(|| {
|
||||
json!({
|
||||
"closed": d.str("arcType").eq_ignore_ascii_case("closed"),
|
||||
"lengthX": d.f64("lengthX", 100.0),
|
||||
"lengthY": d.f64("lengthY", 100.0),
|
||||
"reference": arc_reference(d),
|
||||
"baseAlong": d.str("baseAlong"),
|
||||
"slope": d.f64("slope", 50.0),
|
||||
"fillArc": d.bool("fillArc"),
|
||||
})
|
||||
})
|
||||
});
|
||||
let (x, y) = (d.f64("x", 0.0), d.f64("y", 0.0));
|
||||
let (bx, by) = box_origin(d);
|
||||
let (id, params) = match d.kind.as_str() {
|
||||
@@ -148,15 +218,19 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
("shape.line", json!({"x1": x, "y1": y, "x2": x + l * a.cos(), "y2": y - l * a.sin()}))
|
||||
}
|
||||
"arc" => {
|
||||
let (l, a) = (d.f64("length", 100.0), d.f64("angle", 0.0).to_radians());
|
||||
let anchor = Point::new(d.f64("__anchorX", x), d.f64("__anchorY", y));
|
||||
(
|
||||
"shape.arc",
|
||||
json!({
|
||||
"x1": x,
|
||||
"y1": y,
|
||||
"x2": x + l * a.cos(),
|
||||
"y2": y - l * a.sin(),
|
||||
"closed": d.bool("closed"),
|
||||
"x": anchor.x,
|
||||
"y": anchor.y,
|
||||
"lengthX": d.f64("lengthX", 100.0),
|
||||
"lengthY": d.f64("lengthY", 100.0),
|
||||
"reference": arc_reference(d),
|
||||
"baseAlong": d.str("baseAlong"),
|
||||
"slope": d.f64("slope", 50.0),
|
||||
"closed": d.str("arcType").eq_ignore_ascii_case("closed"),
|
||||
"fillArc": d.bool("fillArc"),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1719,10 +1719,17 @@ fn arc_click_opens_options_and_ok_draws() {
|
||||
use vectorcraft_doc::NodeKind;
|
||||
let mut app = app();
|
||||
open_tool_dialog(&mut app, "arc", json!({"x": 100.0, "y": 50.0}));
|
||||
assert_eq!(app.ui.dialog.as_ref().unwrap().kind, "arc");
|
||||
let d = app.ui.dialog.as_ref().unwrap();
|
||||
assert_eq!(d.kind, "arc");
|
||||
assert!(d.fields.contains_key("lengthX") && d.fields.contains_key("lengthY"));
|
||||
super::confirm(&mut app).unwrap();
|
||||
let n = app.session.doc().unwrap().doc.layers[0].children().unwrap().last().unwrap();
|
||||
assert!(matches!(n.kind, NodeKind::Path { .. }));
|
||||
let pd = n.path_data().unwrap();
|
||||
let a0 = pd.subpaths[0].anchors[0].p;
|
||||
let a1 = pd.subpaths[0].anchors[1].p;
|
||||
assert!((a0.x - 100.0).abs() < 1e-6 && (a0.y - 50.0).abs() < 1e-6);
|
||||
assert!((a1.x - 200.0).abs() < 1e-6 && (a1.y - 150.0).abs() < 1e-6);
|
||||
}
|
||||
|
||||
/// #1377: Polar Grid's dialog has a skew slider under each divider count, Create Compound Path
|
||||
|
||||
@@ -20,16 +20,28 @@ pub fn open_tool_dialog(app: &mut VectorcraftApp, kind: &str, p: Value) {
|
||||
Dialog::new(kind, json!({"x": x, "y": y, "radius1": "50 pt", "radius2": "25 pt", "points": app.session.tool_options_of(kind)["points"]}))
|
||||
}
|
||||
"lineSegment" => Dialog::new(kind, json!({"x": x, "y": y, "length": "100 pt", "angle": 0})),
|
||||
"arc" => Dialog::new(
|
||||
kind,
|
||||
json!({
|
||||
"x": x,
|
||||
"y": y,
|
||||
"length": "100 pt",
|
||||
"angle": 0,
|
||||
"closed": app.session.tool_options_of(kind).get("closed").and_then(Value::as_bool).unwrap_or(false),
|
||||
}),
|
||||
),
|
||||
"arc" => {
|
||||
let opts = app.session.tool_options_of(kind);
|
||||
let closed = opts.get("closed").and_then(Value::as_bool).unwrap_or(false);
|
||||
let mut d = Dialog::new(
|
||||
kind,
|
||||
json!({
|
||||
"__anchorX": x,
|
||||
"__anchorY": y,
|
||||
"x": x,
|
||||
"y": y,
|
||||
"lengthX": opts.get("lengthX").cloned().unwrap_or(json!("100 pt")),
|
||||
"lengthY": opts.get("lengthY").cloned().unwrap_or(json!("100 pt")),
|
||||
"reference": opts.get("reference").cloned().unwrap_or(json!(0)),
|
||||
"arcType": if closed { "closed" } else { "open" },
|
||||
"baseAlong": opts.get("baseAlong").cloned().unwrap_or(json!("x")),
|
||||
"slope": opts.get("slope").cloned().unwrap_or(json!(50)),
|
||||
"fillArc": opts.get("fillArc").and_then(Value::as_bool).unwrap_or(false),
|
||||
}),
|
||||
);
|
||||
super::shapes::arc_pin_to_anchor(&mut d);
|
||||
d
|
||||
}
|
||||
// The grids: the tool's stored options (divider counts and skews, switches) at the default size.
|
||||
"rectangularGrid" | "polarGrid" => {
|
||||
let mut obj = match app.session.tool_options_of(kind) {
|
||||
|
||||
Reference in New Issue
Block a user