Merge pull request #1174 from storytold/fix/1111-bend-blend-spine
#1111: a blend's spine bends into a curve
This commit is contained in:
commit
2adf4465d6
File diff suppressed because one or more lines are too long
|
|
@ -825,20 +825,9 @@ fn reshape_segment(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
s.edit("Reshape", |d, _| {
|
||||
let path = path_mut(d, id)?;
|
||||
let sp = path.subpaths.get_mut(si).ok_or_else(|| EngineError::Other("no such subpath".into()))?;
|
||||
if seg >= sp.segment_count() {
|
||||
if !sp.bend_segment(seg, t, dv) {
|
||||
return Err(EngineError::Other("no such segment".into()));
|
||||
}
|
||||
let n = sp.anchors.len();
|
||||
// Moving both inner control points by v moves B(t) by 3t(1-t)·v. A smooth anchor stays
|
||||
// smooth: its other handle turns with the moved one.
|
||||
let v = dv / (3.0 * t * (1.0 - t));
|
||||
let (i0, i1) = (seg % n, (seg + 1) % n);
|
||||
if let Some(a) = sp.anchors.get_mut(i0) {
|
||||
a.set_handle(true, a.h_out + v, false);
|
||||
}
|
||||
if let Some(b) = sp.anchors.get_mut(i1) {
|
||||
b.set_handle(false, b.h_in + v, false);
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
ok()
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ use vectorcraft_doc::live::{
|
|||
PreserveShape, Spine,
|
||||
};
|
||||
use vectorcraft_doc::{Appearance, Document, Node, NodeId, NodeKind, Selection};
|
||||
use vectorcraft_geom::{Affine, PathData, Point};
|
||||
use vectorcraft_geom::{Affine, PathData, Point, Vec2};
|
||||
|
||||
use super::edit::selected_roots;
|
||||
use super::expand::gradient_sampler;
|
||||
|
|
@ -95,6 +95,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
has_doc,
|
||||
spine_move_anchor
|
||||
),
|
||||
cmd!(
|
||||
"object.blend.spine.reshapeSegment",
|
||||
"Reshape Spine Segment",
|
||||
[],
|
||||
None,
|
||||
"{id? (default: the selected blend), segment: index, t: 0..1, dx, dy} bend a segment of the blend's spine so its point at t moves by (dx, dy), as path.reshapeSegment bends a path's: a straight spine turns into a curve (#1111)",
|
||||
has_doc,
|
||||
spine_reshape_segment
|
||||
),
|
||||
cmd!(
|
||||
"object.blend.spine.addAnchor",
|
||||
"Add Spine Point",
|
||||
|
|
@ -640,6 +649,18 @@ fn spine_move_anchor(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
ok()
|
||||
}
|
||||
|
||||
fn spine_reshape_segment(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "object.blend.spine.reshapeSegment";
|
||||
let seg = index_param(p, "segment", C)?;
|
||||
let t = f64_or(p, "t", 0.5).clamp(0.05, 0.95);
|
||||
let dv = Vec2::new(f64_or(p, "dx", 0.0), f64_or(p, "dy", 0.0));
|
||||
if !(t.is_finite() && dv.x.is_finite() && dv.y.is_finite() && dv.hypot() < 1e9) {
|
||||
return Err(bad(C, "t, dx and dy must be finite"));
|
||||
}
|
||||
edit_spine(s, p, C, "Reshape Spine", |sp, _, _| if sp.bend_segment(seg, t, dv) { Ok(()) } else { Err(bad(C, "no such spine segment")) })?;
|
||||
ok()
|
||||
}
|
||||
|
||||
fn spine_add_anchor(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "object.blend.spine.addAnchor";
|
||||
let q = finite_point(p, C)?;
|
||||
|
|
|
|||
|
|
@ -187,3 +187,20 @@ fn key_anchors_round_trip() {
|
|||
let old: BlendSpec = serde_json::from_value(json!({"spine": {"subpaths": []}})).unwrap();
|
||||
assert!(old.key_anchors.is_empty());
|
||||
}
|
||||
|
||||
/// #1111: a straight spine's segment bends into a curve, its middle moving where it's dragged, and
|
||||
/// the steps follow it; junk segments and deltas are refused.
|
||||
#[test]
|
||||
fn a_straight_spine_segment_bends_into_a_curve() {
|
||||
let mut s = session();
|
||||
let (_, _, g) = blend(&mut s);
|
||||
s.execute("object.blend.spine.reshapeSegment", &json!({"segment": 0, "t": 0.5, "dx": 0, "dy": 40})).unwrap();
|
||||
let spine = spec(&s, g).spine.unwrap();
|
||||
let seg = spine.subpaths[0].segment(0);
|
||||
use vectorcraft_geom::kurbo::ParamCurve;
|
||||
assert!(seg.eval(0.5).distance(Point::new(55.0, 45.0)) < 1e-9, "{seg:?}");
|
||||
assert!(center(&steps(&s, g)[1]).y > 20.0, "the step follows the curve");
|
||||
for bad in [json!({"segment": 1, "dy": 10}), json!({"segment": 0, "dx": f64::MAX, "dy": f64::MAX}), json!({"segment": -1})] {
|
||||
assert!(s.execute("object.blend.spine.reshapeSegment", &bad).is_err(), "{bad}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -367,6 +367,7 @@ fn structured_junk() {
|
|||
("object.blend.options", json!({"spacing": "distance", "value": -1e308, "orientation": 5})),
|
||||
("object.blend.spine.moveAnchor", json!({"id": 2, "anchor": u64::MAX, "x": 1e308, "y": -1e308, "handle": "sideways"})),
|
||||
("object.blend.spine.addAnchor", json!({"id": u64::MAX, "x": 0, "y": 0})),
|
||||
("object.blend.spine.reshapeSegment", json!({"id": 2, "segment": u64::MAX, "t": f64::NAN, "dx": 1e308, "dy": -1e308})),
|
||||
("object.blend.spine.removeAnchor", json!({"anchor": -1})),
|
||||
("object.puppetWarp", json!({"rest": true, "pins": [], "moved": []})),
|
||||
("object.puppetWarp", json!({"rest": true, "pins": [[1e308, 0]], "moved": [[0, 0]], "expand": -5})),
|
||||
|
|
|
|||
|
|
@ -252,6 +252,24 @@ impl SubPath {
|
|||
_ => a,
|
||||
}
|
||||
}
|
||||
/// Bend segment `seg` so its point at `t` moves by `dv`: both inner control points move by
|
||||
/// dv / 3t(1-t), since B(t) moves by 3t(1-t) times that. A smooth anchor stays smooth: its other
|
||||
/// handle turns with the moved one. False when there is no such segment.
|
||||
pub fn bend_segment(&mut self, seg: usize, t: f64, dv: kurbo::Vec2) -> bool {
|
||||
let n = self.anchors.len();
|
||||
if seg >= self.segment_count() || n == 0 {
|
||||
return false;
|
||||
}
|
||||
let v = dv / (3.0 * t * (1.0 - t));
|
||||
if let Some(a) = self.anchors.get_mut(seg % n) {
|
||||
a.set_handle(true, a.h_out + v, false);
|
||||
}
|
||||
if let Some(b) = self.anchors.get_mut((seg + 1) % n) {
|
||||
b.set_handle(false, b.h_in + v, false);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Split segment `seg` at parameter `t`, inserting a new anchor. Returns the new anchor index.
|
||||
pub fn insert_anchor(&mut self, seg: usize, t: f64) -> usize {
|
||||
let n = self.anchors.len();
|
||||
|
|
|
|||
|
|
@ -90,6 +90,15 @@ enum State {
|
|||
start: Point,
|
||||
began: bool,
|
||||
},
|
||||
/// Dragging a curved segment of a selected blend's spine: it follows the pointer
|
||||
/// (`object.blend.spine.reshapeSegment`, #1111).
|
||||
SpineSegment {
|
||||
id: NodeId,
|
||||
seg: usize,
|
||||
t: f64,
|
||||
start: Point,
|
||||
began: bool,
|
||||
},
|
||||
/// Dragging a handle of the clicked spine point.
|
||||
SpineHandle {
|
||||
id: NodeId,
|
||||
|
|
@ -170,6 +179,15 @@ fn hit_spine_point(cx: &ToolContext, p: Point, tol: f64) -> Option<(NodeId, usiz
|
|||
})
|
||||
}
|
||||
|
||||
/// The segment of a selected blend's spine passing within `tol` of `p`: (blend, segment, t, whether
|
||||
/// it is straight).
|
||||
pub(crate) fn hit_spine_segment(cx: &ToolContext, p: Point, tol: f64) -> Option<(NodeId, usize, f64, bool)> {
|
||||
spines(cx).into_iter().filter(|(id, _)| cx.selection.contains(*id)).find_map(|(id, path)| {
|
||||
let (si, seg, t, _, dist) = path.nearest(p)?;
|
||||
(dist <= tol).then(|| (id, seg, t, path.subpaths.get(si).is_some_and(|sp| sp.segment_is_line(seg))))
|
||||
})
|
||||
}
|
||||
|
||||
/// The spine point `sel` and whether `p` is on its outgoing handle's end (`Some(true)`) or its
|
||||
/// incoming one's.
|
||||
fn hit_spine_handle(cx: &ToolContext, sel: (NodeId, usize), p: Point, tol: f64) -> Option<bool> {
|
||||
|
|
@ -402,6 +420,11 @@ impl Tool for DirectSelectionTool {
|
|||
if let Some((id, anchors)) = hit_frame_edge(cx, p, tol) {
|
||||
return self.press_type_area(cx, id, anchors, p, ev.mods.shift);
|
||||
}
|
||||
// A curved segment of a selected blend's spine reshapes, as a path's does.
|
||||
if let Some((id, seg, t, false)) = hit_spine_segment(cx, p, tol) {
|
||||
self.state = State::SpineSegment { id, seg, t, start: p, began: false };
|
||||
return vec![];
|
||||
}
|
||||
if let Some(out) = self.guide.press(cx, ev) {
|
||||
self.state = State::Idle;
|
||||
return out;
|
||||
|
|
@ -546,6 +569,22 @@ impl Tool for DirectSelectionTool {
|
|||
out.push(Action::Preview("object.blend.spine.moveAnchor".into(), json!({"id": id.0, "anchor": anchor, "x": q.x, "y": q.y})));
|
||||
out
|
||||
}
|
||||
(PointerKind::Drag, State::SpineSegment { id, seg, t, start, began }) => {
|
||||
let mut out = vec![];
|
||||
if !began {
|
||||
if p.distance(start) < cx.tol(3.0) {
|
||||
return out;
|
||||
}
|
||||
out.push(Action::Begin("Reshape Spine".into()));
|
||||
self.state = State::SpineSegment { id, seg, t, start, began: true };
|
||||
}
|
||||
let d = p - start;
|
||||
out.push(Action::Preview(
|
||||
"object.blend.spine.reshapeSegment".into(),
|
||||
json!({"id": id.0, "segment": seg, "t": t, "dx": d.x, "dy": d.y}),
|
||||
));
|
||||
out
|
||||
}
|
||||
(PointerKind::Drag, State::SpineHandle { id, anchor, out }) => vec![Action::Preview(
|
||||
"object.blend.spine.moveAnchor".into(),
|
||||
json!({"id": id.0, "anchor": anchor, "handle": if out {"out"} else {"in"}, "x": p.x, "y": p.y, "independent": ev.mods.alt}),
|
||||
|
|
@ -583,6 +622,7 @@ impl Tool for DirectSelectionTool {
|
|||
| State::MoveObject { began, .. }
|
||||
| State::Segment { began, .. }
|
||||
| State::SpinePoint { began, .. }
|
||||
| State::SpineSegment { began, .. }
|
||||
| State::TypeArea { began, .. },
|
||||
) => {
|
||||
self.state = State::Idle;
|
||||
|
|
@ -814,6 +854,45 @@ mod tests {
|
|||
assert!(t.overlays(&cx).iter().any(|o| matches!(o, Overlay::Anchor { filled: true, .. })), "the clicked point shows");
|
||||
}
|
||||
|
||||
/// #1111: the Anchor Point tool bends a selected blend's straight spine; Direct Selection
|
||||
/// reshapes a curved spine segment and leaves a straight one alone, as it does a path's.
|
||||
#[test]
|
||||
fn spine_segments_bend_with_the_anchor_point_tool_and_direct_selection() {
|
||||
let (mut d, g, _) = blend_doc();
|
||||
let mut s = Selection::default();
|
||||
s.set([g]);
|
||||
let p = paint();
|
||||
let drag = |t: &mut dyn crate::Tool, cx: &ToolContext| {
|
||||
t.pointer(cx, &PointerEvent::new(PointerKind::Down, 160.0, 311.0));
|
||||
let a = t.pointer(cx, &PointerEvent::new(PointerKind::Drag, 160.0, 351.0));
|
||||
t.pointer(cx, &PointerEvent::new(PointerKind::Up, 160.0, 351.0));
|
||||
a
|
||||
};
|
||||
let straight = cx(&d, &s, &p);
|
||||
let a = drag(crate::create("anchorPoint").as_mut(), &straight);
|
||||
assert!(
|
||||
matches!(a.last(), Some(Action::Preview(c, v)) if c == "object.blend.spine.reshapeSegment" && v["id"] == g.0 && v["segment"] == 0 && v["dy"] == 40.0),
|
||||
"{a:?}"
|
||||
);
|
||||
let a = drag(&mut DirectSelectionTool::new(false), &straight);
|
||||
assert!(!a.iter().any(|x| matches!(x, Action::Preview(c, _) if c == "object.blend.spine.reshapeSegment")), "straight: {a:?}");
|
||||
// The spine bent upward: Direct Selection takes it from where it passes.
|
||||
let mut sp = vectorcraft_geom::SubPath::polyline(&[Point::new(110.0, 310.0), Point::new(210.0, 310.0)], false);
|
||||
sp.bend_segment(0, 0.5, vectorcraft_geom::Vec2::new(0.0, 20.0));
|
||||
let Some(NodeKind::Blend { spec, .. }) = d.node_mut(g).map(|n| &mut n.kind) else { panic!("blend") };
|
||||
(spec.spine, spec.key_anchors) = (Some(PathData::single(sp)), vec![0, 1]);
|
||||
let mut t = DirectSelectionTool::new(false);
|
||||
let curved = cx(&d, &s, &p);
|
||||
t.pointer(&curved, &PointerEvent::new(PointerKind::Down, 160.0, 330.0));
|
||||
let a = t.pointer(&curved, &PointerEvent::new(PointerKind::Drag, 160.0, 345.0));
|
||||
assert_eq!(a[0], Action::Begin("Reshape Spine".into()));
|
||||
assert!(
|
||||
matches!(&a[1], Action::Preview(c, v) if c == "object.blend.spine.reshapeSegment" && v["segment"] == 0 && v["dx"] == 0.0 && v["dy"] == 15.0),
|
||||
"{a:?}"
|
||||
);
|
||||
assert_eq!(t.pointer(&curved, &PointerEvent::new(PointerKind::Up, 160.0, 345.0)), vec![Action::Commit]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_and_group_selection_pick_blend_keys() {
|
||||
let (d, g, k1) = blend_doc();
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
//! Delete: click an anchor to remove it, re-fitting the neighbouring curve (Alt = add).
|
||||
//! Anchor Point: click a smooth anchor → corner; drag an anchor → pull out smooth handles; drag a
|
||||
//! handle → move it independently (Shift: at 45° steps round its anchor); drag a segment → reshape
|
||||
//! the curve.
|
||||
//! the curve, a selected blend's spine's too, straight or curved (#1111).
|
||||
//! Scissors: click a path to split it there.
|
||||
|
||||
use serde_json::json;
|
||||
|
|
@ -19,9 +19,30 @@ use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, Tool
|
|||
#[derive(Clone, Copy, Debug)]
|
||||
enum State {
|
||||
Idle,
|
||||
Convert { id: NodeId, si: usize, ai: usize, start: Point, began: bool },
|
||||
Handle { id: NodeId, si: usize, ai: usize, out: bool, began: bool },
|
||||
Reshape { id: NodeId, si: usize, seg: usize, t: f64, start: Point, began: bool },
|
||||
Convert {
|
||||
id: NodeId,
|
||||
si: usize,
|
||||
ai: usize,
|
||||
start: Point,
|
||||
began: bool,
|
||||
},
|
||||
Handle {
|
||||
id: NodeId,
|
||||
si: usize,
|
||||
ai: usize,
|
||||
out: bool,
|
||||
began: bool,
|
||||
},
|
||||
/// Bending a segment: a path's, or (`spine`) a selected blend's spine's.
|
||||
Reshape {
|
||||
id: NodeId,
|
||||
si: usize,
|
||||
seg: usize,
|
||||
t: f64,
|
||||
start: Point,
|
||||
began: bool,
|
||||
spine: bool,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct AnchorTool {
|
||||
|
|
@ -94,7 +115,9 @@ impl Tool for AnchorTool {
|
|||
} else if let Some((id, si, ai)) = hit_anchor(cx, p, point) {
|
||||
self.state = State::Convert { id, si, ai, start: p, began: false };
|
||||
} else if let Some((id, si, seg, t)) = hit_segment(cx, p, tol) {
|
||||
self.state = State::Reshape { id, si, seg, t, start: p, began: false };
|
||||
self.state = State::Reshape { id, si, seg, t, start: p, began: false, spine: false };
|
||||
} else if let Some((id, seg, t, _)) = crate::direct::hit_spine_segment(cx, p, tol) {
|
||||
self.state = State::Reshape { id, si: 0, seg, t, start: p, began: false, spine: true };
|
||||
}
|
||||
vec![]
|
||||
}
|
||||
|
|
@ -128,6 +151,10 @@ impl Tool for AnchorTool {
|
|||
json!({"id": id.0, "subpath": si, "anchor": ai, "which": if out { "out" } else { "in" }, "x": q.x, "y": q.y, "independent": true}),
|
||||
)
|
||||
}
|
||||
State::Reshape { id, seg, t, start, spine: true, .. } => Action::Preview(
|
||||
"object.blend.spine.reshapeSegment".into(),
|
||||
json!({"id": id.0, "segment": seg, "t": t, "dx": p.x - start.x, "dy": p.y - start.y}),
|
||||
),
|
||||
State::Reshape { id, si, seg, t, start, .. } => Action::Preview(
|
||||
"path.reshapeSegment".into(),
|
||||
json!({"id": id.0, "subpath": si, "segment": seg, "t": t, "dx": p.x - start.x, "dy": p.y - start.y}),
|
||||
|
|
|
|||
|
|
@ -1545,6 +1545,7 @@
|
|||
Move Rows Zeilen verschieben
|
||||
Move Slices Slices verschieben
|
||||
Move Spine Point Achsenpunkt verschieben
|
||||
Reshape Spine Segment Achsensegment umformen
|
||||
Move Swatch Farbfeld verschieben
|
||||
Move Up Nach oben verschieben
|
||||
Move… Verschieben…
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -1563,6 +1563,7 @@
|
|||
Move Rows Mover filas
|
||||
Move Slices Mover sectores
|
||||
Move Spine Point Mover punto del eje
|
||||
Reshape Spine Segment Remodelar segmento del eje
|
||||
Move Swatch Mover muestra
|
||||
Move Up Subir
|
||||
Move… Mover…
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -1562,6 +1562,7 @@
|
|||
Move Rows Déplacer les lignes
|
||||
Move Slices Déplacer les tranches
|
||||
Move Spine Point Déplacer le point de la dorsale
|
||||
Reshape Spine Segment Modeler le segment de la dorsale
|
||||
Move Swatch Déplacer la nuance
|
||||
Move Up Monter
|
||||
Move… Déplacer…
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -1563,6 +1563,7 @@
|
|||
Move Rows Sposta righe
|
||||
Move Slices Sposta sezioni
|
||||
Move Spine Point Sposta punto dorsale
|
||||
Reshape Spine Segment Rimodella segmento della dorsale
|
||||
Move Swatch Sposta campione
|
||||
Move Up Sposta su
|
||||
Move… Sposta…
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -1697,6 +1697,7 @@
|
|||
Move Rows 行を移動
|
||||
Move Slices スライスを移動
|
||||
Move Spine Point スパインポイントを移動
|
||||
Reshape Spine Segment スパインセグメントをシェイプ変形
|
||||
Move Swatch スウォッチを移動
|
||||
Move Up 上へ移動
|
||||
Move in Perspective 遠近で移動
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -1334,6 +1334,7 @@
|
|||
Move Plane to Match Object Mover Plano para Corresponder ao Objeto
|
||||
Move Slices Mover Fatias
|
||||
Move Spine Point Mover Ponto de Coluna
|
||||
Reshape Spine Segment Remodelar Segmento da Coluna
|
||||
Move Swatch Mover Amostra
|
||||
Move Up Mover para Cima
|
||||
Move in Perspective Mover em Perspectiva
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -1561,6 +1561,7 @@
|
|||
Move Rows Переместить строки
|
||||
Move Slices Переместить фрагменты
|
||||
Move Spine Point Переместить точку остова
|
||||
Reshape Spine Segment Изменить форму сегмента остова
|
||||
Move Swatch Переместить образец
|
||||
Move Up Переместить вверх
|
||||
Move… Переместить…
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -1557,6 +1557,7 @@
|
|||
Move Rows Перемістити рядки
|
||||
Move Slices Перемістити фрагменти
|
||||
Move Spine Point Перемістити точку осі
|
||||
Reshape Spine Segment Змінити форму сегмента осі
|
||||
Move Swatch Перемістити зразок
|
||||
Move Up Перемістити вгору
|
||||
Move… Перемістити…
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 13.
|
|
|
@ -1494,6 +1494,7 @@
|
|||
Move Plane to Match Object 移动平面以符合对象
|
||||
Move Slices 移动切片
|
||||
Move Spine Point 移动脊线点
|
||||
Reshape Spine Segment 整形脊线线段
|
||||
Move Swatch 移动色板
|
||||
Move Up 上移
|
||||
Move… 移动…
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -1538,6 +1538,7 @@
|
|||
Move Rows 移動列
|
||||
Move Slices 移動切片
|
||||
Move Spine Point 移動脊線點
|
||||
Reshape Spine Segment 改變脊線線段外框
|
||||
Move Swatch 移動色票
|
||||
Move Up 上移
|
||||
Move… 移動…
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -692,7 +692,9 @@ new blends start with. The Blend tool (`ui.tool.select {tool: "blend"}`): `ui.po
|
|||
another, blends them; down on an anchor point blends from that point; each further object clicked joins the blend.
|
||||
A selected blend shows its spine. With Direct Selection (`directSelection`) a `ui.pointer` drag from a spine point
|
||||
moves it (`object.blend.spine.moveAnchor`, one undo step) and, once a point is clicked, from one of its handles
|
||||
reshapes the curve; Direct and Group Selection pick a blend's key objects. With the Pen tool a click on a selected
|
||||
reshapes the curve, and a drag on a curved spine segment bends it (`object.blend.spine.reshapeSegment`); the Anchor
|
||||
Point tool (`anchorPoint`) bends any segment of a selected blend's spine, straight ones too. Direct and Group Selection
|
||||
pick a blend's key objects. With the Pen tool a click on a selected
|
||||
blend's spine adds a point (on a point no key sits on, deletes it).
|
||||
|
||||
Perspective grid view options: View → Perspective Grid lists Show/Hide Grid, Show/Hide Rulers, Snap to Grid
|
||||
|
|
|
|||
|
|
@ -3077,8 +3077,9 @@ among the objects takes the others in as more key objects, keeping its options,
|
|||
A blend's spine is the straight lines between its key centres until it is edited. `object.blend.info` lists it
|
||||
(`spine.anchors`, `keyAnchors`: the point each key sits on). `object.blend.spine.addAnchor {x, y}` adds a point where
|
||||
the spine passes nearest, `object.blend.spine.moveAnchor {anchor, x, y}` moves a point (a key on it moves along; with
|
||||
`handle: "in" | "out"` it places that handle) and `object.blend.spine.removeAnchor {anchor}` deletes a point no key
|
||||
sits on; the first edit turns the spine into a path. Moving a key moves its end of the spine. Release
|
||||
`handle: "in" | "out"` it places that handle), `object.blend.spine.reshapeSegment {segment, t, dx, dy}` bends a segment
|
||||
so its point at `t` moves by (dx, dy), turning a straight spine into a curve, and `object.blend.spine.removeAnchor
|
||||
{anchor}` deletes a point no key sits on; the first edit turns the spine into a path. Moving a key moves its end of the spine. Release
|
||||
(`object.blend.release`) leaves the spine as a path that paints nothing (`spines` in the result):
|
||||
|
||||
```json
|
||||
|
|
|
|||
Loading…
Reference in New Issue