M8: Tidy up rounds the gap, tidies overlapping rows and grids along their longer axis, as measured

Third Figma run (web app, 2026-11-12): the mean gap rounds half up (X 0, 60, 135, 215 gives 0, 72, 144, 216; 0, 60, 135 gives 0, 68, 136); a tie between repeated gaps takes the smaller; a row is one row group of layers and may overlap along X, a negative gap counting in the mean (X 0, 60, 105, 170 gives 0, 57, 114, 171); a grid is packed along the axis with more groups (2 x 3 by centred columns, 3 x 2 by centred rows); a diagonal, an irregular layout and layers that all overlap are unavailable. The earlier cell rule is removed.

Choices, not measured: a 2 x 2 grid as columns, auto layout flow children unavailable, the 2,000 layer cap, rotated layers by their box, spacing with two layers, no rounding with Snap to pixel grid off. Grid positions along a stack can differ by a pixel from Figma's (it read 52 and 51 for the same mean 51.67).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Fable 5.2 briefed; Sonnet 5.5 measured and wrote at medium
This commit is contained in:
Trevor Brown
2026-11-12 16:32:48 -05:00
co-authored by Claude Sonnet 5.5
parent cfd6bf5273
commit d3f7ae9029
3 changed files with 148 additions and 141 deletions
+96 -116
View File
@@ -1,21 +1,24 @@
//! Tidy up: the moves that even a selection's gaps the way Figma's web app does.
//!
//! Measured in Figma's web app on 2026-11-12 (top-level rectangles): a row, a column and a 2 x 3
//! grid of unequal sizes, and eight runs on the gap. Figma keeps the order and the sizes of every
//! layer and keeps the first layer where it is. A row evens its horizontal gaps and keeps every Y,
//! a column the mirror image. The one gap is the most common existing gap, else the mean of the
//! gaps taken down to a whole number. A grid is tidied by columns: each column is as wide as its
//! widest layer, centred where its layers were, the columns are evened, every layer is centred in
//! its column and every column is stacked from the selection's top. Tidy up is greyed out for fewer
//! than three layers and for layers that overlap.
//! Measured in Figma's web app on 2026-11-12 (top-level rectangles): a row, a column, a 2 x 3 grid
//! and its transpose, and many runs on the gap. Figma keeps the order and the sizes of every layer
//! and keeps the first layer where it is. Rows are the groups of layers whose Y ranges overlap
//! (transitively), columns the groups whose X ranges overlap. A row is one row group of layers
//! side by side: its horizontal gaps are evened and every Y kept; a column is the mirror image.
//! Layers of a row may overlap along X: a negative gap counts in the mean. The one gap is the most
//! common existing gap (the smaller of two equally common ones), else the mean rounded to a whole
//! number, half up. A grid (two or more rows and two or more columns, one layer in every cell) is
//! packed along the axis that has more groups: for 2 rows by 3 columns each column is as wide as
//! its widest layer, centred where its layers were, the columns are evened, every layer is
//! centred in its column and every column is stacked from the selection's top; 3 rows by 2
//! columns is the mirror image. Tidy up is unavailable for fewer than three layers, for layers all
//! in one row and one column (overlapping each other), for a diagonal and for an irregular layout.
//!
//! Not measured, FigCraft's choices: the smaller of two equally common gaps; a mean ending in .5
//! going down; gaps a pixel apart being different gaps; a diagonal being a row; a grid with an
//! empty place; a grid always read by columns; one overlapping pair among other layers making it
//! unavailable; layers in an auto layout flow making it unavailable; the 2,000 layer cap; rotated
//! layers counting by their box; an explicit `spacing` taking two layers; no rounding in a grid
//! with Snap to pixel grid off. A selection that is none of a row, a column or a grid keeps the
//! earlier rule (the cells of the largest layer).
//! Not measured, FigCraft's choices: a 2 x 2 grid packed as columns; layers in an auto layout flow
//! making it unavailable (the frame places them, and the measurement could not tell this from an
//! irregular layout); the 2,000 layer cap; rotated layers counting by their box; gaps a pixel
//! apart being different gaps; an explicit `spacing` taking two layers; no rounding in a grid with
//! Snap to pixel grid off.
use std::collections::BTreeSet;
@@ -30,33 +33,34 @@ const MAX_LAYERS: usize = 2000;
type Item = (NodeId, Rect);
type Move = (NodeId, f64, f64);
/// What a selection is: a row or a column (`vertical`), or a grid packed in units along an axis
/// (`vertical`: the units are rows).
#[derive(Clone, Copy)]
enum Shape {
Line { vertical: bool },
Grid { vertical: bool },
}
/// Whether Tidy up is offered for `ids`: three to 2,000 layers with boxes, none in an auto layout
/// flow, none overlapping another.
/// flow, forming a row, a column or a grid.
pub fn can_tidy(doc: &Document, ids: &[NodeId]) -> bool {
available(doc, ids, 3).is_some()
}
/// The page-space moves that tidy `ids` (empty when unavailable or when nothing would move).
/// `spacing` sets the gap and lets two layers through; `snap` rounds a grid's column positions to
/// whole pixels.
/// `spacing` sets the gap and lets two layers through; `snap` rounds a grid's positions across its
/// packing axis to whole pixels.
pub(crate) fn tidy_moves(doc: &Document, ids: &[NodeId], spacing: Option<f64>, snap: bool) -> Vec<Move> {
let min = if spacing.is_some() { 2 } else { 3 };
let Some(items) = available(doc, ids, min) else { return Vec::new() };
let cols = groups(&items, false);
let rows = groups(&items, true);
let moves = if cols.len() == items.len() {
line_moves(&items, false, spacing)
} else if rows.len() == items.len() {
line_moves(&items, true, spacing)
} else if is_grid(&cols, &rows) {
grid_moves(&cols, spacing, snap)
} else {
irregular_moves(items, spacing)
let Some((items, shape)) = available(doc, ids, min) else { return Vec::new() };
let moves = match shape {
Shape::Line { vertical } => line_moves(&items, vertical, spacing),
Shape::Grid { vertical } => grid_moves(&groups(&items, vertical), vertical, spacing, snap),
};
moves.into_iter().filter(|(_, dx, dy)| !(dx.is_finite() && dy.is_finite()) || dx.abs() > 1e-9 || dy.abs() > 1e-9).collect()
}
fn available(doc: &Document, ids: &[NodeId], min: usize) -> Option<Vec<Item>> {
fn available(doc: &Document, ids: &[NodeId], min: usize) -> Option<(Vec<Item>, Shape)> {
let mut seen = BTreeSet::new();
let ids: Vec<NodeId> = roots(doc, ids).into_iter().filter(|i| seen.insert(*i)).collect();
if ids.len() < min || ids.len() > MAX_LAYERS {
@@ -74,13 +78,30 @@ fn available(doc: &Document, ids: &[NodeId], min: usize) -> Option<Vec<Item>> {
}
items.push((id, b));
}
let overlapping = items.iter().enumerate().any(|(i, a)| items.iter().skip(i + 1).any(|b| overlap(a.1, b.1)));
if overlapping { None } else { Some(items) }
let shape = classify(&items)?;
Some((items, shape))
}
/// Two boxes overlap when they share more than `EPS` on both axes; touching edges do not count.
fn overlap(a: Rect, b: Rect) -> bool {
a.x1.min(b.x1) - a.x0.max(b.x0) > EPS && a.y1.min(b.y1) - a.y0.max(b.y0) > EPS
/// A row: one row group over two or more column groups (layers sharing a column group overlap).
/// A column: the mirror image. A grid: two or
/// more of each, one layer in every cell, packed along the axis with more groups (columns on a tie).
fn classify(items: &[Item]) -> Option<Shape> {
let cols = groups(items, false);
let rows = groups(items, true);
match (cols.len(), rows.len()) {
(c, 1) if c >= 2 && cols.iter().all(|g| overlapping(g)) => Some(Shape::Line { vertical: false }),
(1, r) if r >= 2 && rows.iter().all(|g| overlapping(g)) => Some(Shape::Line { vertical: true }),
(c, r) if c >= 2 && r >= 2 && one_per_cell(&cols, &rows) => Some(Shape::Grid { vertical: r > c }),
_ => None,
}
}
/// Layers that share a group along a row's or column's own axis must overlap each other (50 px
/// squares at X 0, 60, 105, 170: the middle two do), not merely sit in the same band: a header
/// above two layers side by side is irregular.
fn overlapping(group: &[Item]) -> bool {
let both = |a: Rect, b: Rect| a.x1.min(b.x1) - a.x0.max(b.x0) > EPS && a.y1.min(b.y1) - a.y0.max(b.y0) > EPS;
group.iter().enumerate().all(|(i, a)| group.iter().skip(i + 1).all(|b| both(a.1, b.1)))
}
fn span(b: Rect, vertical: bool) -> (f64, f64) {
@@ -110,13 +131,13 @@ fn groups(items: &[Item], vertical: bool) -> Vec<Vec<Item>> {
out
}
/// No two layers share both a column and a row.
fn is_grid(cols: &[Vec<Item>], rows: &[Vec<Item>]) -> bool {
cols.iter().all(|c| rows.iter().all(|r| c.iter().filter(|a| r.iter().any(|b| b.0 == a.0)).count() <= 1))
/// Every (column, row) cell holds exactly one layer.
fn one_per_cell(cols: &[Vec<Item>], rows: &[Vec<Item>]) -> bool {
cols.iter().all(|c| rows.iter().all(|r| c.iter().filter(|a| r.iter().any(|b| b.0 == a.0)).count() == 1))
}
/// The one gap an axis is evened to: `spacing`, a single gap, the most common gap, else the mean
/// taken down to a whole number.
/// The one gap an axis is evened to: `spacing`, a single gap, the most common gap (the smaller of
/// two equally common ones), else the mean rounded to a whole number, half up.
fn even_gap(gaps: &[f64], spacing: Option<f64>) -> f64 {
if let Some(s) = spacing {
return s;
@@ -129,7 +150,7 @@ fn even_gap(gaps: &[f64], spacing: Option<f64>) -> f64 {
match best {
Some((n, g)) if n >= 2 => g,
_ if gaps.is_empty() => 0.0,
_ => (gaps.iter().sum::<f64>() / gaps.len() as f64 + 1e-9).floor(),
_ => (gaps.iter().sum::<f64>() / gaps.len() as f64 + 0.5 + 1e-9).floor(),
}
}
@@ -161,31 +182,34 @@ fn snap_to(v: f64, snap: bool) -> f64 {
if snap { (v + 0.5).floor() } else { v }
}
/// A column box: where it starts and how wide it is (its widest layer, around the middle of the
/// column's X range).
fn column_box(col: &[Item]) -> (f64, f64) {
let lo = col.iter().map(|i| i.1.x0).fold(f64::MAX, f64::min);
let hi = col.iter().map(|i| i.1.x1).fold(f64::MIN, f64::max);
let w = col.iter().map(|i| i.1.width()).fold(0.0, f64::max);
/// A unit's box on the packing axis: where it starts and how wide it is (its widest layer, around
/// the middle of the unit's range).
fn unit_box(unit: &[Item], vertical: bool) -> (f64, f64) {
let lo = unit.iter().map(|i| span(i.1, vertical).0).fold(f64::MAX, f64::min);
let hi = unit.iter().map(|i| span(i.1, vertical).1).fold(f64::MIN, f64::max);
let w = unit.iter().map(|i| span(i.1, vertical).1 - span(i.1, vertical).0).fold(0.0, f64::max);
((lo + hi) / 2.0 - w / 2.0, w)
}
/// Vertical gaps inside every column, pooled.
fn column_gaps(cols: &[Vec<Item>]) -> Vec<f64> {
/// The gaps inside every unit along the other axis, pooled.
fn unit_gaps(units: &[Vec<Item>], vertical: bool) -> Vec<f64> {
let mut out = Vec::new();
for col in cols {
let mut c = col.clone();
c.sort_by(|a, b| a.1.y0.total_cmp(&b.1.y0));
out.extend(c.windows(2).filter_map(|w| match w {
[a, b] => Some(b.1.y0 - a.1.y1),
for unit in units {
let mut u = unit.clone();
u.sort_by(|a, b| span(a.1, !vertical).0.total_cmp(&span(b.1, !vertical).0));
out.extend(u.windows(2).filter_map(|w| match w {
[a, b] => Some(span(b.1, !vertical).0 - span(a.1, !vertical).1),
_ => None,
}));
}
out
}
fn grid_moves(cols: &[Vec<Item>], spacing: Option<f64>, snap: bool) -> Vec<Move> {
let boxes: Vec<(f64, f64)> = cols.iter().map(|c| column_box(c)).collect();
/// A grid packed in `units` (columns, or rows when `vertical`): the units are evened around where
/// they were, every layer is centred in its unit and each unit is laid out from the selection's
/// start on the other axis.
fn grid_moves(units: &[Vec<Item>], vertical: bool, spacing: Option<f64>, snap: bool) -> Vec<Move> {
let boxes: Vec<(f64, f64)> = units.iter().map(|u| unit_box(u, vertical)).collect();
let across: Vec<f64> = boxes
.windows(2)
.filter_map(|w| match w {
@@ -193,68 +217,24 @@ fn grid_moves(cols: &[Vec<Item>], spacing: Option<f64>, snap: bool) -> Vec<Move>
_ => None,
})
.collect();
let gx = even_gap(&across, spacing);
let gy = even_gap(&column_gaps(cols), spacing);
let top = cols.iter().flatten().map(|i| i.1.y0).fold(f64::MAX, f64::min);
let mut left = boxes.first().map_or(0.0, |b| snap_to(b.0, snap));
let g_units = even_gap(&across, spacing);
let g_within = even_gap(&unit_gaps(units, vertical), spacing);
let start = units.iter().flatten().map(|i| span(i.1, !vertical).0).fold(f64::MAX, f64::min);
let mut at = boxes.first().map_or(0.0, |b| snap_to(b.0, snap));
let mut out = Vec::new();
for (col, (_, w)) in cols.iter().zip(&boxes) {
let mut c = col.clone();
c.sort_by(|a, b| a.1.y0.total_cmp(&b.1.y0));
let mut y = top;
for (id, b) in c {
let x = snap_to(left + (w - b.width()) / 2.0, snap);
out.push((id, x - b.x0, y - b.y0));
y += b.height() + gy;
for (unit, (_, w)) in units.iter().zip(&boxes) {
let mut u = unit.clone();
u.sort_by(|a, b| span(a.1, !vertical).0.total_cmp(&span(b.1, !vertical).0));
let mut pos = start;
for (id, b) in u {
let (lo, hi) = span(b, vertical);
let (lo2, hi2) = span(b, !vertical);
let d_main = snap_to(at + (w - (hi - lo)) / 2.0, snap) - lo;
let d_cross = pos - lo2;
pos += (hi2 - lo2) + g_within;
out.push(if vertical { (id, d_cross, d_main) } else { (id, d_main, d_cross) });
}
left += w + gx;
at += w + g_units;
}
out
}
/// The earlier rule, unchanged: rows by overlapping vertical centres on a grid whose every cell is
/// the largest layer's size, from the union's top left, at the average horizontal gap (at least 10).
fn irregular_moves(mut items: Vec<Item>, spacing: Option<f64>) -> Vec<Move> {
items.sort_by(|a, b| a.1.y0.total_cmp(&b.1.y0));
let mut rows: Vec<Vec<Item>> = Vec::new();
for it in items {
match rows.last_mut() {
Some(row) if row.iter().any(|(_, b)| it.1.center().y < b.y1 && it.1.center().y > b.y0) => row.push(it),
_ => rows.push(vec![it]),
}
}
let all: Vec<Rect> = rows.iter().flatten().map(|(_, b)| *b).collect();
let (Some(origin), maxw, maxh) = (
all.iter().copied().reduce(|a, b| a.union(b)),
all.iter().map(Rect::width).fold(0.0, f64::max),
all.iter().map(Rect::height).fold(0.0, f64::max),
) else {
return Vec::new();
};
let gap = spacing.unwrap_or_else(|| average_gap(&rows));
let mut out = Vec::new();
for (ri, row) in rows.iter_mut().enumerate() {
row.sort_by(|a, b| a.1.x0.total_cmp(&b.1.x0));
for (ci, (id, b)) in row.iter().enumerate() {
let x = origin.x0 + ci as f64 * (maxw + gap);
let y = origin.y0 + ri as f64 * (maxh + gap);
out.push((*id, x - b.x0, y - b.y0));
}
}
out
}
/// The average existing horizontal gap of the widest row, at least 10.
fn average_gap(rows: &[Vec<Item>]) -> f64 {
10f64.max(
rows.iter()
.filter(|r| r.len() > 1)
.map(|r| {
(r.iter().map(|(_, b)| b.x1).fold(f64::MIN, f64::max)
- r.iter().map(|(_, b)| b.x0).fold(f64::MAX, f64::min)
- r.iter().map(|(_, b)| b.width()).sum::<f64>())
/ (r.len() - 1) as f64
})
.fold(0.0, f64::max),
)
}
+49 -22
View File
@@ -107,17 +107,41 @@ fn the_gap_is_the_most_common_one_else_the_mean() {
}
}
/// Within a pixel: Figma read 52 for the vertical gap in one column and 51 in two (the mean of 60,
/// 40, 55 is 51.67), probably from pixel snapping, so a layer's place along a stack may differ by one.
fn near(got: &[(f64, f64)], want: &[(f64, f64)]) {
assert_eq!(got.len(), want.len());
for (g, w) in got.iter().zip(want) {
assert!((g.0 - w.0).abs() <= 1.0 && (g.1 - w.1).abs() <= 1.0, "{got:?} vs {want:?}");
}
}
#[test]
fn a_grid_is_tidied_by_columns() {
let mut s = Session::new();
let ids = grid(&mut s);
tidy(&mut s, &ids);
assert_eq!(all_xy(&s, &ids), vec![(6., -3.), (93., -3.), (203., -3.), (1., 78.), (108., 98.), (195., 88.)]);
near(&all_xy(&s, &ids), &[(6., -3.), (93., -3.), (203., -3.), (1., 79.), (108., 98.), (195., 88.)]);
assert_eq!(size(&s, ids[4]), (40., 50.));
}
#[test]
fn two_layers_and_overlapping_layers_are_left_alone() {
fn a_transposed_grid_is_tidied_by_rows() {
let mut s = Session::new();
let ids = vec![
make(&mut s, 0., 0., 30., 50.),
make(&mut s, 5., 80., 50., 70.),
make(&mut s, -3., 190., 40., 40.),
make(&mut s, 90., 2., 20., 60.),
make(&mut s, 95., 85., 50., 40.),
make(&mut s, 92., 200., 35., 55.),
];
tidy(&mut s, &ids);
near(&all_xy(&s, &ids), &[(-3., 6.), (-3., 93.), (-3., 203.), (78., 1.), (98., 108.), (88., 195.)]);
}
#[test]
fn two_layers_and_a_selection_in_one_cell_are_left_alone() {
let mut s = Session::new();
let two = squares(&mut s, &[0., 87.]);
let before = (all_xy(&s, &two), s.undo_depth());
@@ -149,31 +173,33 @@ fn two_equally_common_gaps_give_the_smaller() {
let mut s = Session::new();
let ids = squares(&mut s, &[0., 60., 120., 220., 320.]);
tidy(&mut s, &ids);
assert_eq!(xs_of(&s, &ids), vec![0., 60., 120., 180., 240.], "not measured in Figma: a tie takes the smaller gap");
assert_eq!(xs_of(&s, &ids), vec![0., 60., 120., 180., 240.]);
}
#[test]
fn a_mean_is_taken_down_not_rounded() {
fn a_mean_is_rounded_half_up() {
let mut s = Session::new();
let ids = squares(&mut s, &[0., 60., 135.]);
tidy(&mut s, &ids);
assert_eq!(xs_of(&s, &ids), vec![0., 67., 134.], "not measured in Figma: a mean of 17.5 goes down");
assert_eq!(xs_of(&s, &ids), vec![0., 68., 136.], "a mean of 17.5 goes up");
let mut s = Session::new();
let ids = squares(&mut s, &[0., 60., 135., 215.]);
tidy(&mut s, &ids);
assert_eq!(xs_of(&s, &ids), vec![0., 71., 142., 213.], "not measured in Figma: a mean of 21.67 goes down");
assert_eq!(xs_of(&s, &ids), vec![0., 72., 144., 216.], "a mean of 21.67 goes to 22");
}
#[test]
fn layers_on_a_diagonal_are_a_row() {
fn layers_on_a_diagonal_are_unavailable() {
let mut s = Session::new();
let ids = vec![make(&mut s, 0., 0., 50., 50.), make(&mut s, 60., 70., 50., 50.), make(&mut s, 140., 150., 50., 50.)];
let before = (all_xy(&s, &ids), s.undo_depth());
tidy(&mut s, &ids);
assert_eq!(all_xy(&s, &ids), vec![(0., 0.), (70., 70.), (140., 150.)], "not measured in Figma: a diagonal is a row");
assert_eq!((all_xy(&s, &ids), s.undo_depth()), before);
assert!(!can(&s, &ids));
}
#[test]
fn a_grid_with_an_empty_place_stacks_each_column() {
fn a_grid_with_an_empty_place_is_unavailable() {
let mut s = Session::new();
let ids = vec![
make(&mut s, 0., 0., 50., 50.),
@@ -182,18 +208,19 @@ fn a_grid_with_an_empty_place_stacks_each_column() {
make(&mut s, 0., 80., 50., 50.),
make(&mut s, 72., 90., 50., 50.),
];
let before = (all_xy(&s, &ids), s.undo_depth());
tidy(&mut s, &ids);
assert_eq!(all_xy(&s, &ids), vec![(0., 0.), (75., 0.), (150., 0.), (0., 85.), (75., 85.)], "not measured in Figma: an empty place in a grid");
assert_eq!((all_xy(&s, &ids), s.undo_depth()), before, "not measured in Figma: an empty place in a grid");
assert!(!can(&s, &ids), "not measured in Figma: an empty place in a grid");
}
#[test]
fn one_overlapping_pair_makes_tidy_up_unavailable() {
fn an_overlapping_pair_in_a_row_is_tidied_and_its_negative_gap_counts() {
let mut s = Session::new();
let ids = squares(&mut s, &[0., 60., 105., 170.]);
let before = (xs_of(&s, &ids), s.undo_depth());
assert!(can(&s, &ids));
tidy(&mut s, &ids);
assert_eq!((xs_of(&s, &ids), s.undo_depth()), before, "not measured in Figma: one overlapping pair");
assert!(!can(&s, &ids), "not measured in Figma: one overlapping pair");
assert_eq!(xs_of(&s, &ids), vec![0., 57., 114., 171.], "gaps 10, -5, 15 have the mean 6.67, which gives 7");
}
#[test]
@@ -224,11 +251,13 @@ fn layers_in_an_auto_layout_flow_are_left_alone() {
}
#[test]
fn an_irregular_selection_keeps_the_earlier_rule() {
fn an_irregular_selection_is_unavailable() {
let mut s = Session::new();
let ids = vec![make(&mut s, 0., 0., 200., 30.), make(&mut s, 0., 50., 90., 50.), make(&mut s, 110., 50., 90., 50.)];
let before = (all_xy(&s, &ids), s.undo_depth());
tidy(&mut s, &ids);
assert_eq!(all_xy(&s, &ids), vec![(0., 0.), (0., 70.), (220., 70.)]);
assert_eq!((all_xy(&s, &ids), s.undo_depth()), before);
assert!(!can(&s, &ids));
}
#[test]
@@ -250,11 +279,9 @@ fn without_pixel_snapping_a_centred_layer_keeps_its_half_pixel() {
s.execute("view.snapPixel", &json!({"on": false})).unwrap();
let ids = grid(&mut s);
tidy(&mut s, &ids);
assert_eq!(
all_xy(&s, &ids),
vec![(6., -3.), (93., -3.), (202.5, -3.), (1., 78.), (108., 98.), (195., 88.)],
"not measured in Figma: no rounding with Snap to pixel grid off"
);
let got = all_xy(&s, &ids);
assert_eq!(got.get(2).map(|p| p.0), Some(202.5), "not measured in Figma: no rounding with Snap to pixel grid off");
near(&got, &[(6., -3.), (93., -3.), (202.5, -3.), (1., 79.), (108., 98.), (195., 88.)]);
}
#[test]
@@ -267,7 +294,7 @@ fn a_rotated_layer_is_tidied_by_its_box() {
let boxes = |s: &Session| -> Vec<figcraft_geom::Rect> { [a, m, c].iter().map(|i| s.doc().unwrap().world_bounds(*i).unwrap()).collect() };
let b = boxes(&s);
let (g1, g2) = (b[1].x0 - b[0].x1, b[2].x0 - b[1].x1);
let want = ((g1 + g2) / 2.0).floor();
let want = ((g1 + g2) / 2.0 + 0.5).floor();
let rot = figcraft_geom::rotation_degrees(s.doc().unwrap().node(m).unwrap().transform);
tidy(&mut s, &[a, m, c]);
let b = boxes(&s);
+3 -3
View File
File diff suppressed because one or more lines are too long