mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-14 04:10:55 +00:00
Merge upstream/main into docs/drag-modifiers-measured
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -113,8 +113,12 @@ pub(super) fn set(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
// A new rotation for the selection turns it about its moved rotation origin: each layer's
|
||||
// point under the origin stays put.
|
||||
let ds = s.ds()?;
|
||||
let pivot =
|
||||
ds.moved_origin().filter(|_| props.contains_key("rotation") && !props.contains_key("x") && !props.contains_key("y") && ds.selection == ids);
|
||||
let pivot = ds.moved_origin().filter(|_| {
|
||||
(props.contains_key("rotation") || props.contains_key("absoluteRotation"))
|
||||
&& !props.contains_key("x")
|
||||
&& !props.contains_key("y")
|
||||
&& ds.selection == ids
|
||||
});
|
||||
let pinned: Vec<(NodeId, figcraft_geom::Point)> = match pivot {
|
||||
Some(o) => roots(&ds.doc, &ids)
|
||||
.into_iter()
|
||||
@@ -161,6 +165,11 @@ pub(super) fn set(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
let doc = s.doc_mut()?;
|
||||
let before = crate::cmd::styles::bound_before(doc, &ids);
|
||||
let mut ids = ids;
|
||||
if props.contains_key("absoluteRotation") {
|
||||
// A page angle depends on the parents': turn a selected parent before the layers in it.
|
||||
ids.sort_by_key(|i| doc.ancestors(*i).len());
|
||||
}
|
||||
for i in ids {
|
||||
crate::cmd::props::apply("node.set", doc, i, skipped.get(&i).unwrap_or(&props))?;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ use crate::{Result, bad};
|
||||
pub const STROKE_SIDES: [&str; 4] = ["strokeTopWeight", "strokeRightWeight", "strokeBottomWeight", "strokeLeftWeight"];
|
||||
|
||||
/// Keys `node.set` understands (documented for agents).
|
||||
pub const KEYS: &str = "name, visible, locked, opacity (0-1 or \"50%\"), blend, x, y (relative to the parent frame; groups don't count), width, height, rotation (degrees), \
|
||||
pub const KEYS: &str = "name, visible, locked, opacity (0-1 or \"50%\"), blend, x, y (relative to the parent frame; groups don't count), width, height, rotation (degrees), absoluteRotation (degrees on the page: the layer's angle plus its parents', what the Design panel's field shows; one of the two per call), \
|
||||
fills (paints or colours; gradients as {type: linearGradient|radialGradient|angularGradient|diamondGradient, stops: [\"#hex\", ...], from?, to?} too), fill (colour, {$var: variable id or name}, or \"none\"/null: no fill), strokes, stroke (colour, {$var: variable id or name}, or \"none\"/null: no stroke), strokeWeight, strokeAlign (inside|center|outside), strokeSides [t,r,b,l], strokeTopWeight / strokeRightWeight / strokeBottomWeight / strokeLeftWeight (one side; turns on individual strokes), \
|
||||
strokeCap / startCap / endCap, strokeJoin, miterAngle (degrees: sharper joins bevel; or miterLimit as a ratio), dashes [..], dashCap (none|round|square; null: dashes take startCap and endCap), widthProfile (preset uniform|wedge|taper|eye|rounded|pinch, [[t, scale], ...] or null), brush (preset id, {preset, spacing, sizeJitter, rotationJitter, scatter, seed} or null), cornerRadius (n or [tl,tr,br,bl]), cornerSmoothing (0-1), effects, \
|
||||
clip, isMask, layout (null|horizontal|vertical|grid|{...}), gap, crossGap, padding (n or [t,r,b,l]), paddingTop/Right/Bottom/Left, \
|
||||
@@ -456,7 +456,7 @@ fn apply_order(k: &str, v: &Value) -> u8 {
|
||||
match k {
|
||||
"path" | "width" | "height" | "w" | "h" => 0,
|
||||
"layout" | "layoutMode" => 1,
|
||||
"rotation" => 3,
|
||||
"rotation" | "absoluteRotation" => 3,
|
||||
"x" | "y" => 4,
|
||||
"fillStyle" | "strokeStyle" | "effectStyle" | "textStyle" | "gridStyle" => 5,
|
||||
"boundVariables" => 6,
|
||||
@@ -595,6 +595,27 @@ fn apply_geometry(
|
||||
let n = doc.get_mut(id)?;
|
||||
n.transform = figcraft_geom::set_rotation_about_center(n.transform, n.width, n.height, deg);
|
||||
}
|
||||
"absoluteRotation" => {
|
||||
let deg = f64_of(v).ok_or_else(|| bad(cmd, "`absoluteRotation` must be degrees"))?;
|
||||
if props.contains_key("rotation") {
|
||||
return Err(bad(cmd, "give one of `rotation` and `absoluteRotation`"));
|
||||
}
|
||||
let (w, h) = {
|
||||
let n = doc.get(id)?;
|
||||
(n.width, n.height)
|
||||
};
|
||||
let world = figcraft_geom::set_rotation_about_center(doc.world_transform(id), w, h, deg);
|
||||
let parent = doc.get(id)?.parent;
|
||||
let pw = parent.map(|p| doc.world_transform(p)).unwrap_or(figcraft_geom::Affine::IDENTITY);
|
||||
let local = pw.inverse() * world;
|
||||
if local.as_coeffs().iter().all(|c| c.is_finite()) {
|
||||
doc.get_mut(id)?.transform = local;
|
||||
} else {
|
||||
// A degenerate parent can't be inverted: turn the local angle instead.
|
||||
let n = doc.get_mut(id)?;
|
||||
n.transform = figcraft_geom::set_rotation_about_center(n.transform, n.width, n.height, deg);
|
||||
}
|
||||
}
|
||||
_ => return Ok(false),
|
||||
}
|
||||
Ok(true)
|
||||
|
||||
@@ -90,6 +90,17 @@ fn rotation_field_and_rotate_commands_turn_about_a_moved_origin() {
|
||||
assert!(s.execute("transform.setRotationOrigin", &json!({"x": 1})).is_err(), "x without y");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_absolute_rotation_key_turns_about_a_moved_origin_too() {
|
||||
let mut s = Session::new();
|
||||
let r = rect(&mut s);
|
||||
s.execute("transform.setRotationOrigin", &json!({"x": 100, "y": 100})).unwrap();
|
||||
s.execute("node.set", &json!({"ids": [r], "props": {"absoluteRotation": 30}})).unwrap();
|
||||
let doc = s.doc().unwrap();
|
||||
assert!(near(doc.world_transform(r) * Point::new(100.0, 100.0), Point::new(100.0, 100.0)));
|
||||
assert!((figcraft_geom::rotation_degrees(doc.world_transform(r)) - 30.0).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn origin_lasts_as_long_as_its_selection() {
|
||||
let mut s = Session::new();
|
||||
|
||||
@@ -533,6 +533,61 @@ fn design_panel_edits_reach_every_selected_layer() {
|
||||
assert!(near(rotation(&s, a), 30.0) && flipped(&s, a), "{}", rotation(&s, a));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn absolute_rotation_sets_the_rotation_on_the_page_keeping_the_centre() {
|
||||
let mut s = session();
|
||||
let page = |s: &Session, i: NodeId| rotation_degrees(s.doc().unwrap().world_transform(i));
|
||||
let f = id(&ex(&mut s, "frame.create", json!({"x": 0, "y": 100, "width": 400, "height": 400})));
|
||||
set(&mut s, &[f], json!({"rotation": 10}));
|
||||
let r = rect(&mut s, 100.0, 100.0, 120.0, 80.0, Some(f));
|
||||
assert!(near(page(&s, r), 10.0) && near(rotation(&s, r), 0.0));
|
||||
let centre = s.doc().unwrap().world_transform(r) * Point::new(60.0, 40.0);
|
||||
set(&mut s, &[r], json!({"absoluteRotation": 7}));
|
||||
assert!(near(page(&s, r), 7.0), "{}", page(&s, r));
|
||||
assert!(near(rotation(&s, r), -3.0), "{}", rotation(&s, r));
|
||||
let c = s.doc().unwrap().world_transform(r) * Point::new(60.0, 40.0);
|
||||
assert!((c - centre).hypot() < 1e-6, "the centre stays put");
|
||||
set(&mut s, &[r], json!({"absoluteRotation": 0}));
|
||||
assert!(near(page(&s, r), 0.0) && near(rotation(&s, r), -10.0));
|
||||
// `node.get` still reports the local angle.
|
||||
let got = ex(&mut s, "node.get", json!({"id": r}));
|
||||
let rot = got["rotation"].as_f64().unwrap_or(f64::NAN);
|
||||
assert!(near(rot, -10.0), "{got}");
|
||||
// Groups: a child reads and sets the page angle, its sibling keeps its own.
|
||||
let a = rect(&mut s, 0.0, 600.0, 50.0, 50.0, None);
|
||||
let b = rect(&mut s, 100.0, 600.0, 50.0, 50.0, None);
|
||||
let g = id(&ex(&mut s, "object.group", json!({"ids": [a, b]})));
|
||||
set(&mut s, &[g], json!({"rotation": 10}));
|
||||
assert!(near(page(&s, a), 10.0));
|
||||
set(&mut s, &[a], json!({"absoluteRotation": 7}));
|
||||
assert!(near(page(&s, a), 7.0) && near(page(&s, b), 10.0));
|
||||
// Several layers: one value on the page for all, one undo step.
|
||||
let top = rect(&mut s, 800.0, 0.0, 50.0, 50.0, None);
|
||||
let steps = undo_steps(&s);
|
||||
set(&mut s, &[r, top], json!({"absoluteRotation": 45}));
|
||||
assert!(near(page(&s, r), 45.0) && near(page(&s, top), 45.0));
|
||||
assert_eq!(undo_steps(&s), steps + 1);
|
||||
// A flip is kept.
|
||||
ex(&mut s, "object.flipH", json!({"ids": [r]}));
|
||||
set(&mut s, &[r], json!({"absoluteRotation": 30}));
|
||||
assert!(near(page(&s, r), 30.0) && flipped(&s, r), "{}", page(&s, r));
|
||||
// One of the two keys per call.
|
||||
assert!(s.execute("node.set", &json!({"ids": [r], "props": {"rotation": 1, "absoluteRotation": 2}})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn absolute_rotation_on_a_frame_and_a_layer_inside_it_lands_both_on_the_page_angle() {
|
||||
let mut s = session();
|
||||
let page = |s: &Session, i: NodeId| rotation_degrees(s.doc().unwrap().world_transform(i));
|
||||
let f = id(&ex(&mut s, "frame.create", json!({"x": 0, "y": 100, "width": 400, "height": 400})));
|
||||
set(&mut s, &[f], json!({"rotation": 10}));
|
||||
let r = rect(&mut s, 100.0, 100.0, 120.0, 80.0, Some(f));
|
||||
// The layer inside comes first in the ids: the frame's turn must not carry it past 45.
|
||||
set(&mut s, &[r, f], json!({"absoluteRotation": 45}));
|
||||
assert!(near(page(&s, f), 45.0), "{}", page(&s, f));
|
||||
assert!(near(page(&s, r), 45.0), "{}", page(&s, r));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constrain_proportions_keeps_the_other_side_in_ratio() {
|
||||
let mut s = session();
|
||||
|
||||
@@ -208,7 +208,7 @@ fn tools() -> Vec<Value> {
|
||||
tool(
|
||||
"set_properties",
|
||||
"Set properties",
|
||||
"Set properties on one or more layers as one undo step (engine `node.set`). Keys: name, visible, locked, opacity (0-1), blend, x, y (relative to the parent frame; groups don't count), width, height, rotation (degrees), fill (colour) / fills (array of paints: \"#hex\" or {\"type\":\"solid\",\"color\":\"#hex\"} or {\"type\":\"linearGradient\",\"stops\":[\"#hex\",\"#hex\"]} or stops [{\"position\":0..1,\"color\":\"#hex\"}], optional from/to handles in the unit box; radialGradient too), stroke / strokes, strokeWeight, strokeAlign (inside|center|outside), cornerRadius (n or [tl,tr,br,bl]), effects (array, e.g. {\"type\":\"dropShadow\",\"color\":\"#00000040\",\"offset\":[0,4],\"radius\":8}), clip, layout (null|horizontal|vertical|grid), gap, padding (n or [t,r,b,l]), mainAlign (min|center|max|spaceBetween|spaceAround|spaceEvenly), crossAlign, wrap, contentAlign (auto|spaceBetween: wrapped lines), sizingH / sizingV (fixed|hug|fill), constraints {horizontal, vertical}, characters, fontFamily, fontStyle, fontSize, lineHeight, letterSpacing, textAlign, textAutoResize. Auto layout keys (gap, padding, mainAlign, crossAlign, wrap, …) apply to frames, components and instances only: other layers among the ids are skipped and named in `warnings`, and with none that can take them the call is an error. A fontStyle draws in the family's nearest face; `warnings` says when that face has another weight or slant than the name asks for. Run list_commands with filter \"node.set\" for the full list. Unknown keys are an error. Returns the updated layers.",
|
||||
"Set properties on one or more layers as one undo step (engine `node.set`). Keys: name, visible, locked, opacity (0-1), blend, x, y (relative to the parent frame; groups don't count), width, height, rotation (degrees), absoluteRotation (degrees on the page), fill (colour) / fills (array of paints: \"#hex\" or {\"type\":\"solid\",\"color\":\"#hex\"} or {\"type\":\"linearGradient\",\"stops\":[\"#hex\",\"#hex\"]} or stops [{\"position\":0..1,\"color\":\"#hex\"}], optional from/to handles in the unit box; radialGradient too), stroke / strokes, strokeWeight, strokeAlign (inside|center|outside), cornerRadius (n or [tl,tr,br,bl]), effects (array, e.g. {\"type\":\"dropShadow\",\"color\":\"#00000040\",\"offset\":[0,4],\"radius\":8}), clip, layout (null|horizontal|vertical|grid), gap, padding (n or [t,r,b,l]), mainAlign (min|center|max|spaceBetween|spaceAround|spaceEvenly), crossAlign, wrap, contentAlign (auto|spaceBetween: wrapped lines), sizingH / sizingV (fixed|hug|fill), constraints {horizontal, vertical}, characters, fontFamily, fontStyle, fontSize, lineHeight, letterSpacing, textAlign, textAutoResize. Auto layout keys (gap, padding, mainAlign, crossAlign, wrap, …) apply to frames, components and instances only: other layers among the ids are skipped and named in `warnings`, and with none that can take them the call is an error. A fontStyle draws in the family's nearest face; `warnings` says when that face has another weight or slant than the name asks for. Run list_commands with filter \"node.set\" for the full list. Unknown keys are an error. Returns the updated layers.",
|
||||
obj(
|
||||
json!({"ids": ids_schema("Layer ids (default: the selection)"), "props": {"type": "object", "description": "Property values to set"}}),
|
||||
&["props"],
|
||||
|
||||
@@ -441,6 +441,68 @@ fn the_arrow_keys_step_by_the_nudge_amounts_and_fractional_fields_by_their_own()
|
||||
assert!((rot.abs() - 11.0).abs() < 1e-6, "rotation {rot}: the steps stay after Escape");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_rotation_field_shows_and_sets_the_rotation_on_the_page() {
|
||||
let mut s = Session::new();
|
||||
let f = id(s.execute("frame.create", &json!({"x": 0, "y": 100, "width": 400, "height": 400})).unwrap());
|
||||
s.execute("node.set", &json!({"ids": [f], "props": {"rotation": 10}})).unwrap();
|
||||
let r = id(s.execute("shape.rectangle", &json!({"x": 100, "y": 100, "width": 120, "height": 80, "parent": f})).unwrap());
|
||||
s.execute("node.set", &json!({"ids": [r], "props": {"rotation": -10}})).unwrap();
|
||||
let b = id(s.execute("shape.rectangle", &json!({"x": 600, "y": 0, "width": 50, "height": 50})).unwrap());
|
||||
s.execute("select.set", &json!({"ids": [r]})).unwrap();
|
||||
let mut u = Ui::new(s);
|
||||
u.ctx.enable_accesskit();
|
||||
u.frame(vec![]);
|
||||
let page = |u: &Ui, n: NodeId| figcraft_geom::rotation_degrees(u.app.session.doc().unwrap().world_transform(n));
|
||||
let local = |u: &Ui, n: NodeId| figcraft_geom::rotation_degrees(u.app.session.doc().unwrap().node(n).unwrap().transform);
|
||||
assert_eq!(field(&mut u, "Rotation").1, "0°");
|
||||
type_into(&mut u, "Rotation", "7", Key::Enter);
|
||||
assert!((page(&u, r) - 7.0).abs() < 1e-6 && (local(&u, r) + 3.0).abs() < 1e-6, "{} {}", page(&u, r), local(&u, r));
|
||||
assert_eq!(field(&mut u, "Rotation").1, "7°");
|
||||
// Arrows step the shown value.
|
||||
let shift = Modifiers { shift: true, ..Modifiers::NONE };
|
||||
let (p, _) = field(&mut u, "Rotation");
|
||||
click(&mut u, p);
|
||||
key(&mut u, Key::ArrowUp, Modifiers::NONE);
|
||||
key(&mut u, Key::ArrowUp, shift);
|
||||
key(&mut u, Key::Escape, Modifiers::NONE);
|
||||
assert!((page(&u, r) - 18.0).abs() < 1e-6, "{}", page(&u, r));
|
||||
assert_eq!(field(&mut u, "Rotation").1, "18°");
|
||||
// Scrubbing is one undo step.
|
||||
let steps = undo_steps(&u);
|
||||
scrub(&mut u, "Rotation", 15.0, Modifiers::NONE);
|
||||
assert!((page(&u, r) - 33.0).abs() < 1e-6, "{}", page(&u, r));
|
||||
assert_eq!(undo_steps(&u), steps + 1);
|
||||
// Mixed: layers in differently turned parents agree when their page angles do.
|
||||
u.app.session.execute("node.set", &json!({"ids": [b], "props": {"rotation": 33}})).unwrap();
|
||||
u.app.session.execute("select.set", &json!({"ids": [r, b]})).unwrap();
|
||||
assert_eq!(field(&mut u, "Rotation").1, "33°");
|
||||
u.app.session.execute("node.set", &json!({"ids": [b], "props": {"rotation": 20}})).unwrap();
|
||||
assert_eq!(field(&mut u, "Rotation").1, "Mixed");
|
||||
let steps = undo_steps(&u);
|
||||
type_into(&mut u, "Rotation", "+10", Key::Enter);
|
||||
assert!((page(&u, r) - 43.0).abs() < 1e-6 && (page(&u, b) - 30.0).abs() < 1e-6, "{} {}", page(&u, r), page(&u, b));
|
||||
assert_eq!(undo_steps(&u), steps + 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_relative_rotation_edit_turns_a_selected_parent_before_its_child_whatever_the_selection_order() {
|
||||
let mut s = Session::new();
|
||||
let f = id(s.execute("frame.create", &json!({"x": 0, "y": 0, "width": 400, "height": 400})).unwrap());
|
||||
let c = id(s.execute("shape.rectangle", &json!({"x": 100, "y": 100, "width": 120, "height": 80, "parent": f})).unwrap());
|
||||
s.execute("node.set", &json!({"ids": [f], "props": {"rotation": 10}})).unwrap();
|
||||
s.execute("node.set", &json!({"ids": [c], "props": {"absoluteRotation": 30}})).unwrap();
|
||||
// The child first, then its parent.
|
||||
s.execute("select.set", &json!({"ids": [c, f]})).unwrap();
|
||||
let mut u = Ui::new(s);
|
||||
u.ctx.enable_accesskit();
|
||||
u.frame(vec![]);
|
||||
let page = |u: &Ui, n: NodeId| figcraft_geom::rotation_degrees(u.app.session.doc().unwrap().world_transform(n));
|
||||
assert_eq!(field(&mut u, "Rotation").1, "Mixed");
|
||||
type_into(&mut u, "Rotation", "+10", Key::Enter);
|
||||
assert!((page(&u, f) - 20.0).abs() < 1e-6 && (page(&u, c) - 40.0).abs() < 1e-6, "{} {}", page(&u, f), page(&u, c));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arithmetic_commits_tab_moves_on_and_escape_restores() {
|
||||
let (mut u, a, _) = setup();
|
||||
|
||||
@@ -75,7 +75,12 @@ impl FigApp {
|
||||
None => return,
|
||||
};
|
||||
let Ok(doc) = self.session.doc() else { return };
|
||||
let values: Vec<(NodeId, f64)> = ids.iter().filter_map(|i| Some((*i, rel.apply(shown(doc.node(*i)?))?))).collect();
|
||||
let mut values: Vec<(NodeId, f64)> = ids.iter().filter_map(|i| Some((*i, rel.apply(shown(doc.node(*i)?))?))).collect();
|
||||
if key == "absoluteRotation" {
|
||||
// A page angle depends on the parents': turn a selected parent before the layers in it
|
||||
// (as `node.set` orders a multi-id call), whatever order the selection is in.
|
||||
values.sort_by_key(|(i, _)| doc.ancestors(*i).len());
|
||||
}
|
||||
if values.is_empty() || self.session.begin_interaction("Edit").is_err() {
|
||||
return;
|
||||
}
|
||||
@@ -627,8 +632,9 @@ fn y_of(doc: &Document, n: &Node) -> f64 {
|
||||
(doc.frame_relative(n.id).as_coeffs()[5] * 100.0).round() / 100.0
|
||||
}
|
||||
|
||||
fn rot_of(n: &Node) -> f64 {
|
||||
(figcraft_geom::rotation_degrees(n.transform) * 100.0).round() / 100.0
|
||||
/// The layer's rotation on the page (its own angle plus its parents'), what the field shows.
|
||||
fn rot_of(doc: &Document, n: &Node) -> f64 {
|
||||
(figcraft_geom::rotation_degrees(doc.world_transform(n.id)) * 100.0).round() / 100.0
|
||||
}
|
||||
|
||||
pub(crate) fn position_values(ctx: &egui::Context, doc: &Arc<Document>, sel: &[NodeId]) -> PositionValues {
|
||||
@@ -637,7 +643,7 @@ pub(crate) fn position_values(ctx: &egui::Context, doc: &Arc<Document>, sel: &[N
|
||||
can_align: figcraft_engine::cmd::arrange::can_align(doc, sel),
|
||||
x: common(doc, sel, |n| x_of(doc, n)),
|
||||
y: common(doc, sel, |n| y_of(doc, n)),
|
||||
rot: common(doc, sel, rot_of),
|
||||
rot: common(doc, sel, |n| rot_of(doc, n)),
|
||||
parent_al: sel.iter().all(|i| doc.node(*i).and_then(parent).is_some_and(|p| p.layout.is_some())),
|
||||
absolute: common(doc, sel, |n| n.child_layout.absolute),
|
||||
// Constraints: children of frames without auto layout, or ignoring it.
|
||||
@@ -655,7 +661,7 @@ fn position_fields(app: &mut FigApp, ui: &mut Ui, doc: &Arc<Document>, sel: &[No
|
||||
let col = widgets::col_width(ui.available_width());
|
||||
align_row(app, ui, sel, pv.can_align, col);
|
||||
position_row(app, ui, doc, sel, &pv, col);
|
||||
rotation_row(app, ui, sel, pv.rot, col);
|
||||
rotation_row(app, ui, doc, sel, pv.rot, col);
|
||||
if pv.in_frame {
|
||||
constraints_row(app, ui, sel, pv.constraints, col);
|
||||
}
|
||||
@@ -732,12 +738,12 @@ fn position_row(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel: &[NodeId], p
|
||||
}
|
||||
|
||||
/// Rotation, with the rotate and flip buttons.
|
||||
fn rotation_row(app: &mut FigApp, ui: &mut Ui, sel: &[NodeId], rot: Option<f64>, col: f32) {
|
||||
fn rotation_row(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel: &[NodeId], rot: Option<f64>, col: f32) {
|
||||
let ids = sel.to_vec();
|
||||
widgets::sublabels(ui, tr("Rotation"), None);
|
||||
row(ui, 32.0, |ui| {
|
||||
let e = widgets::number_field_mixed(ui, Id::unique("pos_rot"), Lead::Icon(Icon::Angle, tr("Rotation")), rot, "°", col, 1.0);
|
||||
app.field_of(&ids, "rotation", e, rot_of, |v| json!(v));
|
||||
app.field_of(&ids, "absoluteRotation", e, |n| rot_of(doc, n), |v| json!(v));
|
||||
let items = [
|
||||
(Icon::Rotate, tr("Rotate 90° right")),
|
||||
(Icon::FlipH, crate::shortcuts::tip(tr("Flip horizontal"), "object.flipH")),
|
||||
|
||||
@@ -67,6 +67,7 @@ state are answered without a frame (about half a second each), while `ui.screens
|
||||
|
||||
Coordinates are page units, y down. `x`/`y` in `node.set`/`node.create` are relative to the parent
|
||||
frame (groups have no coordinate space of their own).
|
||||
`rotation` in `node.set` is the layer's angle to its parent; `absoluteRotation` sets its rotation on the page (what the Design panel's field shows); a call takes one of the two.
|
||||
|
||||
**Windows.** The desktop app can show files in several windows (`window.new`). Every method takes
|
||||
an optional `window` (an id from `window.list`; `0` is the main window) and then runs in that
|
||||
|
||||
@@ -172,6 +172,7 @@ Or in a project's `.mcp.json`:
|
||||
- `create_node` and `set_properties` take `x`/`y` **relative to the parent frame** (the page when
|
||||
there is none, so they are page coordinates then). Groups and boolean groups have no coordinate
|
||||
space of their own: a layer in a group is placed in the frame (or on the page) the group is in.
|
||||
`rotation` is the layer's angle to its parent (Figma's plugin API `rotation` is the same); `absoluteRotation` sets its rotation on the page, what the Design panel's field shows; a call takes one of the two.
|
||||
`inspect_document` reports `x`/`y` the same way, plus `absoluteBounds` in page coordinates.
|
||||
In `props`, `fill` and `stroke` also accept `"none"` (either case) or `null` as no paint, the
|
||||
same as an empty `fills`/`strokes` list, so an outline-only shape can be made in one call.
|
||||
|
||||
+4
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user