Merge pull request #4415 from dotCooCoo/feat/shaper-drag-out

Shaper: dragging a shape out of a Shaper Group's bounds takes it out of the group
This commit is contained in:
BFlat
2026-10-29 15:41:12 +07:00
committed by GitHub
32 changed files with 422 additions and 6 deletions
+1 -1
View File
@@ -15,7 +15,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Transform",
[],
None,
"{matrix: [a,b,c,d,e,f], copy?: bool, ids?, strokes?: bool, corners?: bool, typeAreas?: bool, anchors?: bool, objects?: bool (Transform Objects, default true; false: the art stays and, with patterns, only its pattern tiles transform), patterns?: bool (Transform Patterns: pattern fills and strokes transform with the art; default: prefs transformPatternTiles)} apply an affine to the selection (or ids); anchors (without ids or copy): paths with anchors selected with Direct Selection have only those anchors and their handles transformed, as the Transform tools and object.move/rotate/scale/reflect/shear do (default: the whole objects); strokes/corners: Scale Strokes & Effects / Scale Corners (default: the preferences); typeAreas: area type among them (not type inside a group) reshapes its frame by the matrix and its text reflows at its size, as a bounding-box handle drag does (default: the type transforms too)",
"{matrix: [a,b,c,d,e,f], copy?: bool, ids?, strokes?: bool, corners?: bool, typeAreas?: bool, anchors?: bool, objects?: bool (Transform Objects, default true; false: the art stays and, with patterns, only its pattern tiles transform), patterns?: bool (Transform Patterns: pattern fills and strokes transform with the art; default: prefs transformPatternTiles), releaseOutside?: bool} apply an affine to the selection (or ids); anchors (without ids or copy): paths with anchors selected with Direct Selection have only those anchors and their handles transformed, as the Transform tools and object.move/rotate/scale/reflect/shear do (default: the whole objects); strokes/corners: Scale Strokes & Effects / Scale Corners (default: the preferences); typeAreas: area type among them (not type inside a group) reshapes its frame by the matrix and its text reflows at its size, as a bounding-box handle drag does (default: the type transforms too); releaseOutside (without copy or anchors): a Shaper Group's original shape that ends wholly outside the bounds the group's original shapes had before takes shaper.releaseSource in the same undo step, named Remove from Shaper Group, as the Selection tool's moves in Construction mode do",
has_doc,
transform
),
+29 -2
View File
@@ -235,13 +235,40 @@ fn resize_type_area(n: &mut Node, xf: Affine) -> bool {
pub(super) fn transform(s: &mut Session, p: &Value) -> Result<Value> {
check_switches(p, &TRANSFORM_SWITCHES, "object.transform")?;
check_switches(p, &["anchors"], "object.transform")?;
check_switches(p, &["anchors", "releaseOutside"], "object.transform")?;
let m = matrix_param(p, "matrix").ok_or_else(|| bad("object.transform", "missing matrix [a,b,c,d,e,f]"))?;
let (ids, anchors) = match ids_param(p, "ids") {
Some(v) => (outermost(&s.doc()?.doc, v), false),
None => (selected_roots_to_transform(s)?, bool_or(p, "anchors", false)),
};
apply_transform(s, "Transform", ids, m, p, anchors)
// Shaper's Construction mode: an original shape moved wholly outside its group's bounds leaves
// the group.
let moved = if bool_or(p, "releaseOutside", false) && !bool_or(p, "copy", false) && !anchors {
crate::cmd::shaper::moving_sources(s, &ids)?
} else {
vec![]
};
// Outside a drag and an undo group, the move and the removal are one undo step, named for the
// removal when there is one.
let grouped = !moved.is_empty() && s.doc()?.interaction.is_none() && s.doc()?.undo_group.is_none();
if grouped {
s.begin_named_undo_group(None);
}
let r = apply_transform(s, "Transform", ids, m, p, anchors);
let released = match &r {
Ok(_) if !moved.is_empty() => crate::cmd::shaper::release_outside(s, &moved),
_ => Ok(false),
};
if grouped && let Some(st) = s.active_mut() {
st.undo_group = None;
if matches!(released, Ok(true))
&& let Some(e) = st.history.undo.last_mut()
{
e.label = "Remove from Shaper Group".into();
}
}
released?;
r
}
pub(super) fn move_cmd(s: &mut Session, p: &Value) -> Result<Value> {
+111
View File
@@ -53,6 +53,15 @@ pub fn specs() -> Vec<CommandSpec> {
has_doc,
expand
),
cmd!(
"shaper.releaseSource",
"Remove from Shaper Group",
[],
None,
"{source: id, group?: id (default: the Shaper Group that holds `source`)} take one original shape out of its Shaper Group: it becomes a path just above the group, keeps its look and is selected, and Construction mode ends; the group's result is computed again from the shapes left, and a group left with none is removed. In Construction mode, a Selection-tool move that ends with an original shape wholly outside the bounds the group's original shapes had does the same in the move's undo step (object.transform's `releaseOutside`) → {id}",
has_doc,
release_source
),
]
}
@@ -789,3 +798,105 @@ fn expand(s: &mut Session, p: &Value) -> Result<Value> {
end_isolation(s)?;
Ok(json!({"id": id.0}))
}
/// The editable Shaper Group whose original shapes include `source`.
fn holder(d: &Document, source: NodeId) -> Option<NodeId> {
let sources = d.parent_of(source)?;
let group = d.parent_of(sources)?;
(d.node(group).and_then(source_group).is_some_and(|g| g.id == sources) && d.is_editable(source)).then_some(group)
}
/// Take each of `shapes` (group, original shape) out of its Shaper Group and put it just above that
/// group; the shapes taken out are selected, alone when `only`, else along with what was
/// selected. [`refresh`] then computes each group's result again, or removes a group that holds no
/// shape any more. Construction mode ends when one of those groups was in it.
fn take_out(s: &mut Session, shapes: &[(NodeId, NodeId)], only: bool) -> Result<()> {
let sources: Vec<NodeId> = {
let d = &s.doc()?.doc;
shapes.iter().filter_map(|(group, _)| d.node(*group).and_then(source_group).map(|n| n.id)).collect()
};
s.edit("Remove from Shaper Group", |d, sel| {
// The topmost shape goes first, so each one put just above its group goes below the ones
// that were above it.
let mut order = shapes.to_vec();
order.sort_by_key(|(_, source)| std::cmp::Reverse(d.position(*source).map_or(0, |p| p.1)));
for (group, source) in order {
let node = d.remove(source)?;
let (parent, index, _) = d.position(group).ok_or(EngineError::NoNode(group))?;
d.insert(parent, index + 1, Arc::unwrap_or_clone(node))?;
}
if only {
sel.set(shapes.iter().map(|(_, source)| *source));
}
for &(_, source) in shapes {
if !sel.contains(source) {
sel.objects.push(source);
}
}
Ok(())
})?;
let st = s.doc_mut()?;
if st.isolation.is_some_and(|i| sources.contains(&i)) {
st.isolation = None;
}
Ok(())
}
fn release_source(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "shaper.releaseSource";
let source = id_param(p, "source").ok_or_else(|| bad(C, "source must be the id of a Shaper Group's original shape"))?;
let held = holder(&s.doc()?.doc, source);
let group = match p.get("group") {
None | Some(Value::Null) => held,
Some(_) => Some(id_param(p, "group").ok_or_else(|| bad(C, "group must be the id of a Shaper Group"))?),
};
let group = group.filter(|g| held == Some(*g)).ok_or_else(|| bad(C, "the shape is not an editable original shape of that Shaper Group"))?;
take_out(s, &[(group, source)], true)?;
Ok(json!({"id": source.0}))
}
/// What `object.transform`'s `releaseOutside` checks once the move is made: each of `ids` that is
/// an original shape of an editable Shaper Group, with that group and the bounds the group's
/// original shapes have before the move.
pub(crate) fn moving_sources(s: &Session, ids: &[NodeId]) -> Result<Vec<(NodeId, NodeId, Rect)>> {
let d = &s.doc()?.doc;
Ok(ids
.iter()
.filter_map(|&id| {
let group = holder(d, id)?;
Some((group, id, d.node(group).and_then(source_group).and_then(Node::geometric_bounds)?))
})
.collect())
}
/// Do bounds `b` lie wholly outside `bounds`? Bounds that touch an edge, and flat bounds (a
/// horizontal or vertical line's) on the same line as flat `bounds`, are not outside.
pub(crate) fn wholly_outside(b: Rect, bounds: Rect) -> bool {
b.x0 > bounds.x1 || b.x1 < bounds.x0 || b.y0 > bounds.y1 || b.y1 < bounds.y0
}
/// Take out of their Shaper Groups the `moved` shapes ([`moving_sources`]) that now lie wholly
/// outside the bounds their group's original shapes had before the move. Inside a drag the step
/// is named for that. → whether any shape was taken out.
pub(crate) fn release_outside(s: &mut Session, moved: &[(NodeId, NodeId, Rect)]) -> Result<bool> {
let outside: Vec<(NodeId, NodeId)> = {
let d = &s.doc()?.doc;
moved
.iter()
.filter(|(_, id, before)| d.node(*id).and_then(Node::geometric_bounds).is_some_and(|b| wholly_outside(b, *before)))
.map(|(group, id, _)| (*group, *id))
.collect()
};
if outside.is_empty() {
return Ok(false);
}
// Shapes the group can't be computed from without the ones taken out keep it as it is: the
// edit is rolled back, and the move stays a plain move.
if take_out(s, &outside, false).is_err() {
return Ok(false);
}
if let Some(it) = &mut s.doc_mut()?.interaction {
it.label = "Remove from Shaper Group".into();
}
Ok(true)
}
+159
View File
@@ -611,3 +611,162 @@ fn shaper_joined_outline_keeps_its_stroke_paint_after_source_edits() {
assert_ne!(node(&s, id).appearance.stroke_paint().label(), "#00ff00");
}
}
fn source_ids(s: &Session, g: NodeId) -> Vec<NodeId> {
node(s, g).children().unwrap()[0].children().unwrap().iter().map(|n| n.id).collect()
}
/// A Selection-tool drag from `from` to `to`.
fn drag(s: &mut Session, v: ViewInfo, from: [f64; 2], to: [f64; 2]) {
s.pointer(&PointerEvent::new(PointerKind::Down, from[0], from[1]), v).unwrap();
for t in [0.25, 0.5, 1.0] {
let p = [from[0] + (to[0] - from[0]) * t, from[1] + (to[1] - from[1]) * t];
s.pointer(&PointerEvent::new(PointerKind::Drag, p[0], p[1]), v).unwrap();
}
s.pointer(&PointerEvent::new(PointerKind::Up, to[0], to[1]), v).unwrap();
}
/// Two overlapping rectangles in a Shaper Group, in Construction mode with the left one selected
/// and the Selection tool chosen → (left, right, group).
fn construction(s: &mut Session, v: ViewInfo) -> (NodeId, NodeId, NodeId) {
let a = rectangle(s, 0.0, "#ff0000");
let b = rectangle(s, 50.0, "#0000ff");
let g = scribble(s, &[[25.0, 20.0], [75.0, 35.0], [30.0, 50.0], [125.0, 65.0]]);
s.execute("object.isolate", &json!({"id": g.0})).unwrap();
s.execute("select.set", &json!({"ids": [a.0]})).unwrap();
s.select_tool("selection", v).unwrap();
(a, b, g)
}
/// In Construction mode, a Selection-tool drag that drops an original shape wholly outside the
/// bounds of the group's original shapes takes it out of the group, in one undo step: it becomes a
/// path just above the group with its own look, and stays selected. Undo puts it back. With
/// Real-time Drawing off the drop does the same.
#[test]
fn shaper_dragging_an_original_shape_out_of_the_bounds_takes_it_out_of_the_group() {
for real_time in [true, false] {
let mut s = session();
s.prefs.real_time_drawing = real_time;
let v = ViewInfo { smart_guides: false, ..Default::default() };
let (a, b, g) = construction(&mut s, v);
let look = node(&s, a).appearance.clone();
let steps = s.doc().unwrap().history.undo.len();
drag(&mut s, v, [25.0, 50.0], [25.0, 200.0]);
let st = s.doc().unwrap();
assert_eq!(source_ids(&s, g), vec![b], "real-time {real_time}");
let (parent, index, _) = st.doc.position(a).unwrap();
assert_eq!((parent, index), { st.doc.position(g).map(|(p, i, _)| (p, i + 1)).unwrap() });
assert_eq!(node(&s, a).appearance, look);
assert_eq!(node(&s, a).geometric_bounds().unwrap().y0, 150.0);
assert_eq!(st.selection.objects, vec![a]);
assert_eq!(st.isolation, None);
assert_eq!(st.history.undo.len(), steps + 1);
assert_eq!(st.history.undo.last().unwrap().label, "Remove from Shaper Group");
// The group's result is the right rectangle alone.
assert_eq!(fill(&s, g, 25.0, 50.0), None);
assert_eq!(fill(&s, g, 125.0, 50.0).as_deref(), Some("#0000ff"));
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(source_ids(&s, g), vec![a, b]);
assert_eq!(node(&s, a).geometric_bounds().unwrap().y0, 0.0);
assert_eq!(fill(&s, g, 25.0, 50.0).as_deref(), Some("#ff0000"));
}
}
/// A drop that leaves the original shape across the bounds of the group's original shapes only
/// moves it, as before.
#[test]
fn shaper_dragging_an_original_shape_across_the_bounds_only_moves_it() {
let mut s = session();
let v = ViewInfo { smart_guides: false, ..Default::default() };
let (a, b, g) = construction(&mut s, v);
let sources = node(&s, g).children().unwrap()[0].id;
drag(&mut s, v, [25.0, 50.0], [25.0, 100.0]);
let st = s.doc().unwrap();
assert_eq!(source_ids(&s, g), vec![a, b]);
assert_eq!(node(&s, a).geometric_bounds().unwrap().y0, 50.0);
assert_eq!(st.isolation, Some(sources));
assert_eq!(st.history.undo.last().unwrap().label, "Move");
// Outside Construction mode the same drop moves the group as usual.
s.execute("object.exitIsolation", &json!({})).unwrap();
drag(&mut s, v, [25.0, 100.0], [25.0, 400.0]);
assert_eq!(source_ids(&s, g), vec![a, b]);
}
/// Taking the last original shape out removes the group, and the native file keeps the result.
#[test]
fn shaper_taking_the_last_original_shape_out_removes_the_group_and_saves() {
let mut s = session();
let v = ViewInfo { smart_guides: false, ..Default::default() };
let (a, b, g) = construction(&mut s, v);
drag(&mut s, v, [25.0, 50.0], [25.0, 200.0]);
s.execute("object.isolate", &json!({"id": g.0})).unwrap();
s.execute("select.set", &json!({"ids": [b.0]})).unwrap();
drag(&mut s, v, [125.0, 50.0], [400.0, 50.0]);
let doc = &s.doc().unwrap().doc;
assert!(doc.node(g).is_none());
assert_eq!(doc.parent_of(a), doc.parent_of(b));
assert_eq!(node(&s, b).geometric_bounds().unwrap().x0, 325.0);
let back = vectorcraft_format::load(&vectorcraft_format::save(doc, false)).unwrap();
assert_eq!(back.layers, doc.layers);
}
/// Flat bounds of horizontal or vertical lines on the line of a group's flat bounds, and bounds that
/// touch the group's bounds, are not outside them.
#[test]
fn shaper_outside_test_handles_flat_and_touching_bounds() {
use crate::cmd::shaper::wholly_outside;
use vectorcraft_geom::Rect;
let row = Rect::new(0.0, 150.0, 200.0, 150.0);
assert!(!wholly_outside(Rect::new(50.0, 150.0, 120.0, 150.0), row));
assert!(!wholly_outside(Rect::new(150.0, 150.0, 300.0, 150.0), row));
assert!(wholly_outside(Rect::new(250.0, 150.0, 300.0, 150.0), row));
assert!(wholly_outside(Rect::new(50.0, 160.0, 120.0, 160.0), row));
let column = Rect::new(80.0, 0.0, 80.0, 100.0);
assert!(!wholly_outside(Rect::new(80.0, 50.0, 80.0, 150.0), column));
assert!(wholly_outside(Rect::new(90.0, 50.0, 90.0, 150.0), column));
let square = Rect::new(0.0, 0.0, 100.0, 100.0);
assert!(!wholly_outside(Rect::new(100.0, 0.0, 200.0, 100.0), square));
assert!(wholly_outside(Rect::new(100.5, 0.0, 200.0, 100.0), square));
}
/// `shaper.releaseSource` takes the named original shape out, refuses a shape of another group or
/// none, and `object.transform`'s `releaseOutside` makes one undo step outside a drag too.
#[test]
fn shaper_release_source_command_and_transform_flag() {
let mut s = session();
let a = rectangle(&mut s, 0.0, "#ff0000");
let b = rectangle(&mut s, 50.0, "#0000ff");
let g = scribble(&mut s, &[[25.0, 20.0], [75.0, 35.0], [30.0, 50.0], [125.0, 65.0]]);
let other = rectangle(&mut s, 300.0, "#00ff00");
let untouched = s.doc().unwrap().doc.layers.clone();
for p in [json!({"source": other.0}), json!({"source": a.0, "group": other.0}), json!({"source": "x"}), json!({})] {
assert!(s.execute("shaper.releaseSource", &p).is_err(), "{p}");
assert_eq!(s.doc().unwrap().doc.layers, untouched);
}
s.execute("shaper.releaseSource", &json!({"source": b.0, "group": g.0})).unwrap();
assert_eq!(source_ids(&s, g), vec![a]);
assert_eq!(s.doc().unwrap().selection.objects, vec![b]);
assert_eq!(s.doc().unwrap().history.undo.last().unwrap().label, "Remove from Shaper Group");
s.execute("edit.undo", &json!({})).unwrap();
let steps = s.doc().unwrap().history.undo.len();
s.execute("object.transform", &json!({"matrix": [1, 0, 0, 1, 0, 150], "ids": [a.0], "releaseOutside": true})).unwrap();
assert_eq!(source_ids(&s, g), vec![b]);
assert_eq!(s.doc().unwrap().history.undo.len(), steps + 1);
assert_eq!(s.doc().unwrap().history.undo.last().unwrap().label, "Remove from Shaper Group");
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(source_ids(&s, g), vec![a, b]);
assert_eq!(node(&s, a).geometric_bounds().unwrap().y0, 0.0);
// With a history of one step, that step still goes back to before the move.
s.doc_mut().unwrap().history.limit = 1;
s.execute("object.transform", &json!({"matrix": [1, 0, 0, 1, 0, 150], "ids": [a.0], "releaseOutside": true})).unwrap();
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(source_ids(&s, g), vec![a, b]);
assert_eq!(node(&s, a).geometric_bounds().unwrap().y0, 0.0);
// Shapes taken out together keep their stacking order, whatever order they are named in.
for ids in [[a.0, b.0], [b.0, a.0]] {
s.execute("object.transform", &json!({"matrix": [1, 0, 0, 1, 0, 150], "ids": ids, "releaseOutside": true})).unwrap();
let doc = &s.doc().unwrap().doc;
assert!(doc.node(g).is_none());
assert!(doc.position(a).unwrap().1 < doc.position(b).unwrap().1, "{ids:?}");
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(source_ids(&s, g), vec![a, b]);
}
}
+21 -2
View File
@@ -120,6 +120,9 @@ pub struct SelectionTool {
/// What a bounding-box handle being dragged snaps to.
targets: Option<PointSnap>,
moving: Option<MoveSnap>,
/// The params of the move's last preview: in Shaper's Construction mode the move ends with
/// them and `releaseOutside` ([`construction`]).
last_move: Option<Value>,
/// The move snapped to a point or a guide at its last step: the arrow turns hollow.
snapped: bool,
guide: GuideEdit,
@@ -809,8 +812,17 @@ impl Tool for SelectionTool {
self.measure = None;
self.guides.clear();
self.moving = None;
let last = self.last_move.take();
match (began, deselect, key) {
(true, ..) => vec![Action::Commit],
// In Construction mode, an original shape dropped wholly outside the group's
// bounds leaves the group, in the move's undo step (`releaseOutside`).
(true, ..) => match last.filter(|p| construction(cx) && p["copy"] != json!(true)) {
Some(mut params) => {
params["releaseOutside"] = json!(true);
vec![Action::Preview("object.transform".into(), params), Action::Commit]
}
None => vec![Action::Commit],
},
(false, Some(id), _) => vec![Action::Exec("select.toggle".into(), json!({ "id": id.0 }))],
// A click on the key object again: no key.
(false, None, Some(id)) if cx.selection.key == Some(id) => vec![Action::Exec("select.key".into(), json!({}))],
@@ -965,6 +977,11 @@ fn can_be_key(cx: &ToolContext, id: NodeId) -> bool {
cx.doc.node(id).is_some_and(|n| !n.is_layer()) && !inside_selected && !member
}
/// Shaper's Construction mode: a Shaper Group's original shapes are isolated.
fn construction(cx: &ToolContext) -> bool {
cx.isolation.and_then(|id| cx.doc.node(id)).is_some_and(|n| n.name.as_deref() == Some(vectorcraft_doc::shaper::SOURCES))
}
/// Is `n` area type (text in a frame) whose frame the tools reshape? Type in perspective isn't:
/// it transforms whole.
pub(crate) fn is_area_type(n: &vectorcraft_doc::Node) -> bool {
@@ -1192,7 +1209,9 @@ impl SelectionTool {
self.snapped = (d - unsnapped).hypot() > 1e-9;
self.state = State::Moving { start, press, hold, began: true, deselect: None, key: None };
self.measure = cx.measurement_labels.then(|| (p, cx.offset_label(d.x, d.y)));
out.push(Action::Preview("object.transform".into(), json!({ "matrix": matrix_json(Affine::translate(d)), "copy": m.alt })));
let params = json!({ "matrix": matrix_json(Affine::translate(d)), "copy": m.alt });
self.last_move = Some(params.clone());
out.push(Action::Preview("object.transform".into(), params));
out
}
}
+1
View File
@@ -4410,6 +4410,7 @@ axis Both И двете
Shaper Инструмент за форми
Expand Shaper Group Разширяване на групата от форми
Release Shaper Group Освобождаване на групата от форми
Remove from Shaper Group Премахване от групата от форми
Select Shaper Face Избиране на лице на формата
Drag art, colors and text styles here to reuse them across documents. Libraries are stored on this machine — no account required. Плъзнете графика, цветове и текстови стилове тук, за да ги използвате повторно в различни документи. Библиотеките се съхраняват на това устройство — не се изисква акаунт.
1 Color Job Задание с 1 цвят
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -823,6 +823,10 @@
Shaper Shaper
Expand Shaper Group Rozložit skupinu Shaper
Release Shaper Group Uvolnit skupinu Shaper
Remove from Shaper Group Odebrat ze skupiny Shaper
@msg source must be the id of a Shaper Group's original shape source musí být identifikátor původního tvaru skupiny Shaper
@msg group must be the id of a Shaper Group group musí být identifikátor skupiny Shaper
@msg the shape is not an editable original shape of that Shaper Group tvar není upravitelný původní tvar této skupiny Shaper
Select Shaper Face Vybrat oblast Shaper
Constrain Omezit
Free Transform Volná transformace
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -758,6 +758,7 @@
Shaper Shaper
Expand Shaper Group Udvid shapergruppe
Release Shaper Group Frigør shapergruppe
Remove from Shaper Group Fjern fra shapergruppe
Select Shaper Face Markér shaperflade
Constrain Begræns
Free Transform Fri transformering
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -4440,6 +4440,10 @@ theme Appearance Erscheinungsbild
Shaper Shaper
Expand Shaper Group Shaper-Gruppe umwandeln
Release Shaper Group Shaper-Gruppe zurückwandeln
Remove from Shaper Group Aus Shaper-Gruppe entfernen
@msg source must be the id of a Shaper Group's original shape source muss die ID einer Originalform einer Shaper-Gruppe sein
@msg group must be the id of a Shaper Group group muss die ID einer Shaper-Gruppe sein
@msg the shape is not an editable original shape of that Shaper Group die Form ist keine bearbeitbare Originalform dieser Shaper-Gruppe
Select Shaper Face Shaper-Teilfläche auswählen
Drag art, colors and text styles here to reuse them across documents. Libraries are stored on this machine — no account required. Ziehen Sie Grafiken, Farben und Textformate hierher, um sie in mehreren Dokumenten wiederzuverwenden. Bibliotheken werden auf diesem Computer gespeichert – kein Konto erforderlich.
1 Color Job 1-Farben-Auftrag
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -4472,6 +4472,10 @@ theme Appearance Εμφάνιση
Shaper Διαμορφωτής σχημάτων (Shaper Tool)
Expand Shaper Group Ανάπτυξη ομάδας διαμόρφωσης σχημάτων
Release Shaper Group Αποδέσμευση ομάδας διαμόρφωσης σχημάτων
Remove from Shaper Group Αφαίρεση από ομάδα διαμόρφωσης σχημάτων
@msg source must be the id of a Shaper Group's original shape Το source πρέπει να είναι το αναγνωριστικό ενός αρχικού σχήματος ομάδας διαμόρφωσης σχημάτων
@msg group must be the id of a Shaper Group Το group πρέπει να είναι το αναγνωριστικό μιας ομάδας διαμόρφωσης σχημάτων
@msg the shape is not an editable original shape of that Shaper Group Το σχήμα δεν είναι επεξεργάσιμο αρχικό σχήμα αυτής της ομάδας διαμόρφωσης σχημάτων
Select Shaper Face Επιλογή επιφάνειας διαμορφωτή
Drag art, colors and text styles here to reuse them across documents. Libraries are stored on this machine — no account required. Σύρετε εδώ σχέδια, χρώματα και στυλ κειμένου για χρήση σε άλλα έγγραφα. Οι βιβλιοθήκες αποθηκεύονται στον υπολογιστή — χωρίς λογαριασμό.
1 Color Job Εργασία ενός χρώματος
Can't render this file because it contains an unexpected character in line 2071 and column 19.
+4
View File
@@ -4520,6 +4520,10 @@ axis Both Ambos
Shaper Shaper
Expand Shaper Group Expandir grupo Shaper
Release Shaper Group Soltar grupo Shaper
Remove from Shaper Group Quitar del grupo Shaper
@msg source must be the id of a Shaper Group's original shape source debe ser el identificador de una forma original de un grupo Shaper
@msg group must be the id of a Shaper Group group debe ser el identificador de un grupo Shaper
@msg the shape is not an editable original shape of that Shaper Group la forma no es una forma original editable de ese grupo Shaper
Select Shaper Face Seleccionar cara Shaper
Constrain Restringir
Free Transform Transformación libre
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -809,6 +809,10 @@
Shaper Muotoilija
Expand Shaper Group Laajenna muotoilijaryhmä
Release Shaper Group Vapauta muotoilijaryhmä
Remove from Shaper Group Poista muotoilijaryhmästä
@msg source must be the id of a Shaper Group's original shape source-arvon on oltava muotoilijaryhmän alkuperäisen muodon tunnus
@msg group must be the id of a Shaper Group group-arvon on oltava muotoilijaryhmän tunnus
@msg the shape is not an editable original shape of that Shaper Group muoto ei ole kyseisen muotoilijaryhmän muokattava alkuperäinen muoto
Select Shaper Face Valitse muotoilijan pinta
Constrain Rajoita
Free Transform Vapaa muunnos
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -4517,6 +4517,10 @@ axis Both Les deux
Shaper Shaper
Expand Shaper Group Décomposer le groupe Shaper
Release Shaper Group Annuler le groupe Shaper
Remove from Shaper Group Retirer du groupe Shaper
@msg source must be the id of a Shaper Group's original shape source doit être l’identifiant d’une forme d’origine d’un groupe Shaper
@msg group must be the id of a Shaper Group group doit être l’identifiant d’un groupe Shaper
@msg the shape is not an editable original shape of that Shaper Group la forme n’est pas une forme d’origine modifiable de ce groupe Shaper
Select Shaper Face Sélectionner la face Shaper
Drag art, colors and text styles here to reuse them across documents. Libraries are stored on this machine — no account required. Faites glisser ici des illustrations, des couleurs et des styles de texte pour les réutiliser d’un document à l’autre. Les bibliothèques sont enregistrées sur cet ordinateur — aucun compte n’est nécessaire.
1 Color Job Travail 1 couleur
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -3100,6 +3100,10 @@ mark-style Japanese Japán
Release Opacity Mask Átlátszósági maszk megszüntetése
Release Selection Kijelölés felszabadítása
Release Shaper Group Alakformáló csoport megszüntetése
Remove from Shaper Group Eltávolítás az alakformáló csoportból
@msg source must be the id of a Shaper Group's original shape A source értékének egy alakformáló csoport eredeti alakzatának azonosítójának kell lennie
@msg group must be the id of a Shaper Group A group értékének egy alakformáló csoport azonosítójának kell lennie
@msg the shape is not an editable original shape of that Shaper Group Az alakzat nem szerkeszthető eredeti alakzata ennek az alakformáló csoportnak
Release Text Wrap Körbefuttatás megszüntetése
Release to Layers (Build) Szétosztás rétegekre (építkezve)
Release to Layers (Sequence) Szétosztás rétegekre (sorozatként)
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -758,6 +758,7 @@
Shaper Shaper
Expand Shaper Group Perluas grup Shaper
Release Shaper Group Lepaskan grup Shaper
Remove from Shaper Group Hapus dari grup Shaper
Select Shaper Face Seleksi sisi Shaper
Constrain Batasi
Free Transform Transformasi bebas
1 # Indonesian (id) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation
758
759
760
761
762
763
764
+4
View File
@@ -4560,6 +4560,10 @@ axis Both Entrambi
Shaper Shaper
Expand Shaper Group Espandi gruppo Shaper
Release Shaper Group Rilascia gruppo Shaper
Remove from Shaper Group Rimuovi dal gruppo Shaper
@msg source must be the id of a Shaper Group's original shape source deve essere l’identificatore di una forma originale di un gruppo Shaper
@msg group must be the id of a Shaper Group group deve essere l’identificatore di un gruppo Shaper
@msg the shape is not an editable original shape of that Shaper Group la forma non è una forma originale modificabile di quel gruppo Shaper
Select Shaper Face Seleziona faccia Shaper
Constrain Vincola
Free Transform Trasformazione libera
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -3791,6 +3791,10 @@ theme Appearance 外観
Shaper Shaper
Expand Shaper Group Shaper グループを拡張
Release Shaper Group Shaper グループを解除
Remove from Shaper Group Shaper グループから削除
@msg source must be the id of a Shaper Group's original shape source には Shaper グループの元のシェイプの ID を指定してください
@msg group must be the id of a Shaper Group group には Shaper グループの ID を指定してください
@msg the shape is not an editable original shape of that Shaper Group そのシェイプはその Shaper グループの編集可能な元のシェイプではありません
Select Shaper Face Shaper の面を選択
Constrain 制限
Free Transform 自由変形
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -699,6 +699,10 @@
Shaper ფორმირება
Expand Shaper Group ფორმირების ჯგუფის გაშლა
Release Shaper Group ფორმირების ჯგუფის მოხსნა
Remove from Shaper Group ფორმირების ჯგუფიდან ამოღება
@msg source must be the id of a Shaper Group's original shape source უნდა იყოს ფორმირების ჯგუფის საწყისი ფიგურის იდენტიფიკატორი
@msg group must be the id of a Shaper Group group უნდა იყოს ფორმირების ჯგუფის იდენტიფიკატორი
@msg the shape is not an editable original shape of that Shaper Group ფიგურა არ არის ამ ფორმირების ჯგუფის რედაქტირებადი საწყისი ფიგურა
Select Shaper Face ფორმირების არის მონიშვნა
Constrain შეზღუდვა
Free Transform თავისუფალი გარდაქმნა
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -3778,6 +3778,10 @@ theme Appearance 외관
Shaper 셰이퍼
Expand Shaper Group 셰이퍼 그룹 확장
Release Shaper Group 셰이퍼 그룹 해제
Remove from Shaper Group 셰이퍼 그룹에서 제거
@msg source must be the id of a Shaper Group's original shape source는 셰이퍼 그룹의 원본 모양 ID여야 합니다
@msg group must be the id of a Shaper Group group은 셰이퍼 그룹의 ID여야 합니다
@msg the shape is not an editable original shape of that Shaper Group 모양이 해당 셰이퍼 그룹의 편집 가능한 원본 모양이 아닙니다
Select Shaper Face 셰이퍼 면 선택
Constrain 제한
Free Transform 자유 변형
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -772,6 +772,7 @@
Shaper Shaper
Expand Shaper Group Shapergroep uitbreiden
Release Shaper Group Shapergroep vrijgeven
Remove from Shaper Group Verwijderen uit shapergroep
Select Shaper Face Shapervlak selecteren
Constrain Beperken
Free Transform Vrij transformeren
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -767,6 +767,10 @@
Shaper Kształtowanie
Expand Shaper Group Rozszerz grupę kształtowania
Release Shaper Group Uwolnij grupę kształtowania
Remove from Shaper Group Usuń z grupy kształtowania
@msg source must be the id of a Shaper Group's original shape source musi być identyfikatorem oryginalnego kształtu grupy kształtowania
@msg group must be the id of a Shaper Group group musi być identyfikatorem grupy kształtowania
@msg the shape is not an editable original shape of that Shaper Group kształt nie jest edytowalnym oryginalnym kształtem tej grupy kształtowania
Select Shaper Face Zaznacz ścianę kształtowania
Constrain Ogranicz
Free Transform Swobodne przekształcanie
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -3070,6 +3070,10 @@ imageTrace Outline Contorno
Shaper Formador
Expand Shaper Group Expandir Grupo Formador
Release Shaper Group Liberar Grupo Formador
Remove from Shaper Group Remover do Grupo Formador
@msg source must be the id of a Shaper Group's original shape source deve ser o ID de uma forma original de um Grupo Formador
@msg group must be the id of a Shaper Group group deve ser o ID de um Grupo Formador
@msg the shape is not an editable original shape of that Shaper Group a forma não é uma forma original editável desse Grupo Formador
Select Shaper Face Selecionar Face do Formador
Constrain Restringir
Free Transform Transformação Livre
Can't render this file because it contains an unexpected character in line 6 and column 47.
+4
View File
@@ -4556,6 +4556,10 @@ axis Both Оба
Shaper Shaper
Expand Shaper Group Разобрать группу Shaper
Release Shaper Group Освободить группу Shaper
Remove from Shaper Group Удалить из группы Shaper
@msg source must be the id of a Shaper Group's original shape source должен быть идентификатором исходной фигуры группы Shaper
@msg group must be the id of a Shaper Group group должен быть идентификатором группы Shaper
@msg the shape is not an editable original shape of that Shaper Group фигура не является редактируемой исходной фигурой этой группы Shaper
Select Shaper Face Выбрать область Shaper
Constrain Ограничить
Free Transform Свободное трансформирование
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -4426,6 +4426,10 @@ axis Both Båda
Shaper Formning
Expand Shaper Group Expandera Shaper-grupp
Release Shaper Group Släpp formgivargrupp
Remove from Shaper Group Ta bort från formgivargrupp
@msg source must be the id of a Shaper Group's original shape source måste vara id för en originalform i en formgivargrupp
@msg group must be the id of a Shaper Group group måste vara id för en formgivargrupp
@msg the shape is not an editable original shape of that Shaper Group formen är inte en redigerbar originalform i den formgivargruppen
Select Shaper Face Markera Shaper-yta
Drag art, colors and text styles here to reuse them across documents. Libraries are stored on this machine — no account required. Dra hit grafik, färger och textstilar för att återanvända dem i andra dokument. Biblioteken lagras på den här datorn – inget konto krävs.
1 Color Job 1-färgsjobb
Can't render this file because it contains an unexpected character in line 5 and column 25.
+4
View File
@@ -4271,6 +4271,10 @@ axis Both Her İkisi
Shaper Şekillendirici
Expand Shaper Group Shaper Grubunu Genişlet
Release Shaper Group Şekillendirici Grubunu Yayınla
Remove from Shaper Group Şekillendirici Grubundan Kaldır
@msg source must be the id of a Shaper Group's original shape source, bir Şekillendirici Grubunun özgün şeklinin kimliği olmalıdır
@msg group must be the id of a Shaper Group group, bir Şekillendirici Grubunun kimliği olmalıdır
@msg the shape is not an editable original shape of that Shaper Group şekil, o Şekillendirici Grubunun düzenlenebilir özgün şekli değil
Select Shaper Face Şekillendirici Yüz'ü seçin
Drag art, colors and text styles here to reuse them across documents. Libraries are stored on this machine — no account required. Resimleri, renkleri ve metin stillerini belgelerde yeniden kullanmak için buraya sürükleyin. Kitaplıklar bu makinede depolanır; hesap gerekmez.
1 Color Job 1 Renk İşi
Can't render this file because it contains an unexpected character in line 5 and column 25.
+4
View File
@@ -4542,6 +4542,10 @@ axis Both Обидві
Shaper Формоутворення
Expand Shaper Group Розібрати групу формоутворення
Release Shaper Group Звільнити групу формоутворення
Remove from Shaper Group Вилучити з групи формоутворення
@msg source must be the id of a Shaper Group's original shape source має бути ідентифікатором вихідної фігури групи формоутворення
@msg group must be the id of a Shaper Group group має бути ідентифікатором групи формоутворення
@msg the shape is not an editable original shape of that Shaper Group фігура не є редагованою вихідною фігурою цієї групи формоутворення
Select Shaper Face Виділити область формоутворення
Constrain Обмежити
Free Transform Вільне трансформування
Can't render this file because it contains an unexpected character in line 749 and column 40.
+4
View File
@@ -2875,6 +2875,10 @@ mark-style Japanese Kiểu Nhật
Release Opacity Mask Giải phóng mặt nạ độ mờ
Release Selection Giải phóng vùng chọn
Release Shaper Group Giải phóng nhóm Shaper
Remove from Shaper Group Xóa khỏi nhóm Shaper
@msg source must be the id of a Shaper Group's original shape source phải là mã định danh của một hình gốc trong nhóm Shaper
@msg group must be the id of a Shaper Group group phải là mã định danh của một nhóm Shaper
@msg the shape is not an editable original shape of that Shaper Group hình này không phải là hình gốc có thể chỉnh sửa của nhóm Shaper đó
Release to Layers (Build) Giải phóng ra các lớp (Xây dựng)
Release to Layers (Sequence) Giải phóng ra các lớp (Chuỗi)
Release with Perspective Giải phóng với phối cảnh
1 # Vietnamese (vi) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation
2875
2876
2877 @msg
2878 @msg
2879 @msg
2880
2881
2882
2883
2884
+4
View File
@@ -4831,6 +4831,10 @@ axis Both 两者
The dialog was closed before its file dialog answered 文件对话框给出结果之前,对话框已关闭
Expand Shaper Group 扩展 Shaper 组
Release Shaper Group 释放 Shaper 组
Remove from Shaper Group 从 Shaper 组中移除
@msg source must be the id of a Shaper Group's original shape source 必须是 Shaper 组中原始形状的 ID
@msg group must be the id of a Shaper Group group 必须是 Shaper 组的 ID
@msg the shape is not an editable original shape of that Shaper Group 该形状不是该 Shaper 组中可编辑的原始形状
Select Shaper Face 选择 Shaper 区域
Constrain 约束
Perspective Distort 透视扭曲
Can't render this file because it contains an unexpected character in line 2 and column 73.
+4
View File
@@ -4641,6 +4641,10 @@ axis Both 兩者
Shaper Shaper
Expand Shaper Group 展開 Shaper 群組
Release Shaper Group 釋放 Shaper 群組
Remove from Shaper Group 從 Shaper 群組中移除
@msg source must be the id of a Shaper Group's original shape source 必須是 Shaper 群組中原始形狀的 ID
@msg group must be the id of a Shaper Group group 必須是 Shaper 群組的 ID
@msg the shape is not an editable original shape of that Shaper Group 該形狀不是該 Shaper 群組中可編輯的原始形狀
Select Shaper Face 選取 Shaper 區域
Constrain 強制
Free Transform 任意變形
Can't render this file because it contains an unexpected character in line 6 and column 47.
+14 -1
View File
@@ -1104,7 +1104,7 @@ The transform, Align, Object Properties and Live Shape Properties commands refus
that is not true or false with `` `strokes` must be true or false ``, naming the key, and leave the document and its
undo history as they were: the options `copy`, `strokes`, `corners`, `patterns`, `objects` and `typeAreas` in
`object.transform`, `object.move`, `object.nudge`, `object.rotate`, `object.scale`, `object.reflect`, `object.shear`,
`object.transformAgain` and `object.setBounds`; `anchors` in `object.transform`; `big` in `object.nudge`;
`object.transformAgain` and `object.setBounds`; `anchors` and `releaseOutside` in `object.transform`; `big` in `object.nudge`;
`absolute` in `object.rotate`; `proportional` in `object.setBounds`; `patterns` in `object.align`, `object.distribute`
and `object.distributeSpacing`; `visible`, `locked`, `isolate` and `knockoutShape` in `object.setProps`; and
`invertPie`, `makeSidesEqual` and `relativeRadii` in `object.setLiveShape`. A null is read as a key left out.
@@ -1389,6 +1389,19 @@ everything in it, and `select.all`, `select.allOnArtboard` and `select.inverse`
among the objects beside it in its group or layer, passing over hidden and locked objects and sublayers. When there
is no such object, the selection stays as it is.
## Shaper Groups
`shaper.releaseSource {source, group?}` takes the original shape `source` out of its Shaper Group: the shape becomes a
path just above the group with its own fill and stroke, and is selected. The group's result is computed again from the
original shapes left, and a group left with none is removed. When `group` is given, it must be the Shaper Group that
holds `source`. A source that is not an original shape of an editable Shaper Group is an error, and the document and its
undo history stay as they were. The command returns `{id}`, the shape's id, and records one undo step, Remove from Shaper
Group. Construction mode ends when the group was in it.
`object.transform` with `releaseOutside: true` does the same for each moved original shape whose bounds end wholly
outside the bounds the group's original shapes had before the move, in the move's undo step. A shape whose bounds still
overlap them only moves. The Selection tool sends it at the end of a move in Construction mode.
## Global Edit
Select › Start Global Edit is `select.globalEdit {appearance?, size?, artboards?, includeCanvas?}`. With one path,
+2
View File
@@ -13,6 +13,8 @@ Click once to select the group; click again to select its visible fill and chang
While the group or one of its faces is selected (Face Selection mode) and the Shaper or Selection tool is active, an arrow widget sits beside the middle of the right side of the original shapes’ bounds. Click it to enter Construction mode: the original shapes are isolated and nothing is selected, and a click with Shaper selects the group’s original shape under the pointer. Click the widget again to leave Construction mode with the group selected. The arrow points right in Face Selection mode and left in Construction mode.
To take a shape out of a Shaper Group, drag it with the Selection tool in Construction mode and drop it wholly outside the bounds of the group's original shapes. The shape becomes a path just above the group, keeps its fill and stroke and stays selected, and Construction mode ends. The group is computed again from the shapes left, and a group left with no shapes is removed. The move and the removal are one undo step, Remove from Shaper Group. A shape dropped across the bounds only moves.
Smart Guides and Snap to Point do not snap to the original shapes. The reference point of the transform tools snaps to an original shape only when that shape is selected, or under the pointer in Construction mode. Outside Construction mode the Lasso does not pick the anchor points of the original shapes, and the Add Anchor Point, Delete Anchor Point, Anchor Point and Scissors tools do not pick the anchor points or segments of an original shape that is not selected.
Object › Shaper › Release restores the original paths. Expand keeps the visible result as ordinary paths. Each scribble and source edit records one undo step. Sources in a composition must share a parent layer/group; Shaper limits a composition to 64 source paths and 4,096 segments.
+1
View File
@@ -627,6 +627,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- On Windows a maximized window stops one pixel short of each monitor edge where an auto-hidden taskbar sits.
- **Tools:**
- **Shaper:** uneven squares and rectangles become live rectangles even when drawing begins mid-edge. Recognition uses uniformly spaced samples and closed-loop corners, fits rectangle edges/angles rather than requiring a nearly perfect bounding-box fill, and includes the pointer-release position. Rectangles/squares and ellipses snap to 45° steps, hexagons to 90°, and triangles point up or down; lines keep their free angle even with Shift. Shaper Groups retain the live originals, merge regions with the gesture-origin fill, punch fills independently from strokes, remove boundaries on exiting scribbles and trim protruding stroke pieces. Source moves/resizes re-evaluate the result in the same undo step; double-click enters construction editing, and with the Shaper or Selection tool a click on the arrow widget beside a Shaper Group's original shapes switches the group between Face Selection and Construction mode. Native save/reopen, face recoloring, Release and Expand are covered. Finer gesture heuristics remain.
In Construction mode, a Selection-tool drag that drops an original shape wholly outside the bounds of the group's original shapes takes it out of the group in the move's undo step (Remove from Shaper Group, `shaper.releaseSource`): the shape becomes a path above the group and stays selected, and a group left with no shapes is removed. Engine tests drag a shape out of a two-shape group and back across the bounds, take the last shape out, undo, and save the result.
- **Selection:** Selection, Direct/Group Selection, Magic Wand, Lasso. A Shift-drag marquee toggles what it reaches: objects with the Selection tool, anchors with Direct and Group Selection (#483); the Lasso adds with Shift and subtracts with Alt. Dragging a path segment with Direct Selection bends it when it's curved (its anchors stay) and moves its two anchors when it's straight, one undo step; the fill, Shift or Alt still pick the whole path (Alt-drag copies it).
With the Selection or Group Selection tool, pressing E while dragging a marquee makes the marquee select only the objects that lie entirely inside it. Pressing E again switches the marquee back to its default selection.
The Lasso picks anchor points only on art a click can pick, and the Add Anchor Point, Delete Anchor Point, Anchor Point and Scissors tools pick anchor points and segments only on the selected paths and on art a click can pick: not on a Shaper Group's original shapes outside Construction mode, and in isolation mode only inside the isolated group or object. Smart Guides and Snap to Point do not snap to a Shaper Group's original shapes, and the reference point of the transform tools snaps to one only when it is selected, or under the pointer in Construction mode.