mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-08 19:42:05 +00:00
Region fills: each region once, no override for a no-op, Copy fill reads the region, layer-only controls off region rows
Named regions are deduplicated; an inheriting region stays inheriting when an edit leaves its stack as it was, and its own stack is only replaced once the edit succeeds; fill.copy reads the selected regions' paint; the Fill section leaves the fill style, a variable bound to the layer's fill and the image crop off region rows. Tests in regions_tests and region_fill_rows_tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2e392e99b1
commit
d2e7c49e57
7 files changed
+167
-20
No files matched your search
@@ -587,3 +587,27 @@ fn the_fill_commands_edit_the_selected_regions() {
|
||||
s.undo().unwrap();
|
||||
assert_eq!(layer(&s), before, "one undo step per command");
|
||||
}
|
||||
|
||||
/// Region fill edits from review: a region named twice is edited once; an edit that changes
|
||||
/// nothing leaves an inheriting region inheriting; Copy fill reads the selected region's fill.
|
||||
#[test]
|
||||
fn region_fill_edits_apply_once_skip_no_ops_and_copy_the_region() {
|
||||
let mut s = Session::new();
|
||||
let id = editing_vector(&mut s, overlapping());
|
||||
let fills_of = |s: &Session, r: usize| net(s, id).regions[r].fills.clone();
|
||||
ex(&mut s, "vector.edit.select", json!({"regions": [1]}));
|
||||
ex(&mut s, "fill.move", json!({"ids": [id], "from": 0, "to": 0}));
|
||||
assert_eq!(fills_of(&s, 1), None, "moving a paint onto itself leaves the region inheriting");
|
||||
ex(&mut s, "fill.remove", json!({"ids": [id], "index": 5}));
|
||||
assert_eq!(fills_of(&s, 1), None, "removing a paint that isn't there leaves it inheriting");
|
||||
ex(&mut s, "fill.add", json!({"ids": [id], "regions": [0, 0], "color": "#00FF00"}));
|
||||
assert_eq!(fills_of(&s, 0).map(|f| f.len()), Some(2), "the layer's fill and one added, once");
|
||||
// Copy fill takes the selected region's own paint, not the layer's.
|
||||
ex(&mut s, "fill.set", json!({"ids": [id], "color": "#FF0000"}));
|
||||
ex(&mut s, "fill.copy", json!({"ids": [id], "index": 0}));
|
||||
let copied = s.clipboard.paint.clone().map(|(_, p)| p.solid_color().unwrap().to_css_hex().to_ascii_uppercase());
|
||||
assert_eq!(copied.as_deref(), Some("#FF0000"));
|
||||
// Regions whose fills differ have no one paint to copy.
|
||||
ex(&mut s, "vector.edit.select", json!({"regions": [0, 1]}));
|
||||
assert!(s.execute("fill.copy", &json!({"ids": [id], "index": 0})).is_err());
|
||||
}
|
||||
@@ -273,7 +273,7 @@ pub(crate) fn fill_regions(s: &Session, cmd: &str, p: &Value, ids: &[NodeId]) ->
|
||||
if let Some(list) = get(p, "regions") {
|
||||
let [id] = ids else { return Err(bad(cmd, "`regions` goes with one vector layer")) };
|
||||
let Some(count) = region_count(*id) else { return Err(bad(cmd, "`regions` goes with a vector layer")) };
|
||||
let regions = list
|
||||
let mut regions = list
|
||||
.as_array()
|
||||
.ok_or_else(|| bad(cmd, "regions: [index, ...]"))?
|
||||
.iter()
|
||||
@@ -284,6 +284,9 @@ pub(crate) fn fill_regions(s: &Session, cmd: &str, p: &Value, ids: &[NodeId]) ->
|
||||
.ok_or_else(|| bad(cmd, format!("regions: indices below {count}")))
|
||||
})
|
||||
.collect::<Result<Vec<usize>>>()?;
|
||||
// A region named twice is edited once.
|
||||
regions.sort_unstable();
|
||||
regions.dedup();
|
||||
if regions.is_empty() {
|
||||
return Err(bad(cmd, "regions: at least one"));
|
||||
}
|
||||
@@ -292,12 +295,16 @@ pub(crate) fn fill_regions(s: &Session, cmd: &str, p: &Value, ids: &[NodeId]) ->
|
||||
let [id] = ids else { return Ok(None) };
|
||||
let Some(ve) = s.ds()?.vector_edit.as_ref().filter(|ve| ve.id == *id && !ve.regions.is_empty()) else { return Ok(None) };
|
||||
let count = region_count(*id).unwrap_or(0);
|
||||
let regions: Vec<usize> = ve.regions.iter().copied().filter(|r| *r < count).collect();
|
||||
let mut regions: Vec<usize> = ve.regions.iter().copied().filter(|r| *r < count).collect();
|
||||
regions.sort_unstable();
|
||||
regions.dedup();
|
||||
Ok((!regions.is_empty()).then_some((*id, regions)))
|
||||
}
|
||||
|
||||
/// Runs `f` on the own fill stack of each of `regions` of vector `id` (a region without one
|
||||
/// starts from the layer's fills, which stay as they are).
|
||||
/// starts from the layer's fills, which stay as they are). A region that inherits the layer's
|
||||
/// fills keeps inheriting when `f` leaves the stack as it was, so later edits of the layer's
|
||||
/// fills still reach it; a region's own stack is only replaced once `f` succeeds.
|
||||
pub(crate) fn edit_region_fills(
|
||||
doc: &mut figcraft_doc::Document,
|
||||
id: NodeId,
|
||||
@@ -308,9 +315,11 @@ pub(crate) fn edit_region_fills(
|
||||
let NodeKind::Vector { network } = &mut doc.get_mut(id)?.kind else { return Err(bad("fill", "not a vector layer")) };
|
||||
for &r in regions {
|
||||
let Some(region) = network.regions.get_mut(r) else { continue };
|
||||
let mut list = region.fills.take().unwrap_or_else(|| inherited.clone());
|
||||
let mut list = region.fills.clone().unwrap_or_else(|| inherited.clone());
|
||||
f(&mut list)?;
|
||||
region.fills = Some(list);
|
||||
if region.fills.is_some() || list != inherited {
|
||||
region.fills = Some(list);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -433,7 +442,15 @@ pub(super) fn paint_copy(s: &mut Session, p: &Value, fills: bool) -> Result<Valu
|
||||
let (paint, stroke) = {
|
||||
let doc = s.doc()?;
|
||||
let n = doc.get(id)?;
|
||||
let list = if fills { &n.fills } else { &n.strokes };
|
||||
// Regions selected in vector edit mode: their own fills, as the Fill section lists them
|
||||
// (#1875).
|
||||
let regions = if fills { crate::vectoredit::selected_region_fills(s, id) } else { None };
|
||||
let list = match ®ions {
|
||||
Some(Some(list)) => list,
|
||||
Some(None) => return Err(bad(cmd, "the selected regions' fills differ")),
|
||||
None if fills => &n.fills,
|
||||
None => &n.strokes,
|
||||
};
|
||||
let at = at.unwrap_or(list.len().saturating_sub(1));
|
||||
let paint = list.get(at).cloned().ok_or_else(|| bad(cmd, format!("no {} at index {at}", if fills { "fill" } else { "stroke" })))?;
|
||||
(paint, (!fills).then(|| n.stroke.clone()))
|
||||
|
||||
@@ -221,6 +221,8 @@ mod proto_reorder_tests;
|
||||
#[cfg(test)]
|
||||
mod proto_responsive_tests;
|
||||
#[cfg(test)]
|
||||
mod region_fill_rows_tests;
|
||||
#[cfg(test)]
|
||||
mod resources_tests;
|
||||
#[cfg(test)]
|
||||
mod sampled_color_tests;
|
||||
|
||||
@@ -299,20 +299,20 @@ pub(super) fn paints(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel: &[NodeI
|
||||
[id] if fills => figcraft_engine::vectoredit::selected_region_fills(&app.session, *id),
|
||||
_ => None,
|
||||
};
|
||||
// The fill style, a variable bound to the layer's fill and the image crop are the layer's:
|
||||
// they stay off a region's rows.
|
||||
let region = region_list.is_some();
|
||||
let list = region_list.unwrap_or_else(|| common(doc, sel, |n| if fills { n.fills.clone() } else { n.strokes.clone() }));
|
||||
let title = if fills { tr("Fill") } else { tr("Stroke") };
|
||||
let empty = list.as_ref().is_none_or(|l| l.is_empty());
|
||||
let slot = if fills { StyleSlot::Fill } else { StyleSlot::Stroke };
|
||||
// The section's right-click menu adds the copied paint (`fill.paste` / `stroke.paste`).
|
||||
let paste = if fills { tr("Paste fill") } else { tr("Paste stroke") };
|
||||
let (clicked, header) = widgets::section_header_resp(
|
||||
ui,
|
||||
title,
|
||||
&[(Icon::Styles, tr("Styles")), (Icon::Plus, if fills { tr("Add fill") } else { tr("Add stroke") })],
|
||||
empty,
|
||||
);
|
||||
let add = (Icon::Plus, if fills { tr("Add fill") } else { tr("Add stroke") });
|
||||
let buttons = if region { vec![add] } else { vec![(Icon::Styles, tr("Styles")), add] };
|
||||
let (clicked, header) = widgets::section_header_resp(ui, title, &buttons, empty);
|
||||
if let Some(i) = clicked {
|
||||
if i == 1 {
|
||||
if buttons.get(i).is_some_and(|(icon, _)| *icon == Icon::Plus) {
|
||||
// The new paint goes on top; open its picker. A mixed selection has no one index to edit.
|
||||
if app.run(if fills { "fill.add" } else { "stroke.add" }, json!({"ids": ids})).is_some()
|
||||
&& let Some(l) = &list
|
||||
@@ -329,14 +329,16 @@ pub(super) fn paints(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel: &[NodeI
|
||||
ui.close();
|
||||
}
|
||||
});
|
||||
style_picker::chip(app, ui, doc, &ids, slot);
|
||||
if !region {
|
||||
style_picker::chip(app, ui, doc, &ids, slot);
|
||||
}
|
||||
match &list {
|
||||
None => widgets::note(ui, tr("Click + to replace mixed content"), t.text_secondary),
|
||||
Some(l) => {
|
||||
// The last paint is on top: shown first. Rows drag to reorder.
|
||||
let mut drag = ListDrag::new(ui, if fills { "fills" } else { "strokes" });
|
||||
for (i, p) in l.iter().enumerate().rev() {
|
||||
let r = paint_row(app, ui, &ids, i, p, fills);
|
||||
let r = paint_row(app, ui, &ids, i, p, fills, region);
|
||||
drag.row(ui, i, r, r, || paint_label(p));
|
||||
}
|
||||
if let Some((from, to)) = drag.finish(ui) {
|
||||
@@ -446,7 +448,9 @@ pub(super) fn paints(app: &mut FigApp, ui: &mut Ui, doc: &Document, sel: &[NodeI
|
||||
}
|
||||
|
||||
/// Paint `i`'s row (swatch, colour or kind, opacity, visibility, remove); returns its area.
|
||||
fn paint_row(app: &mut FigApp, ui: &mut Ui, ids: &[NodeId], i: usize, p: &Paint, fills: bool) -> Rect {
|
||||
/// One paint's row; `region`: a row of the regions selected in vector edit mode (#1875), which
|
||||
/// leaves out what belongs to the layer (its variable binding, the image crop).
|
||||
fn paint_row(app: &mut FigApp, ui: &mut Ui, ids: &[NodeId], i: usize, p: &Paint, fills: bool, region: bool) -> Rect {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let ids = ids.to_vec();
|
||||
let cmd = if fills { "fill.set" } else { "stroke.set" };
|
||||
@@ -461,7 +465,7 @@ fn paint_row(app: &mut FigApp, ui: &mut Ui, ids: &[NodeId], i: usize, p: &Paint,
|
||||
let text_r = Rect::from_min_max(pos2(sw.max.x + 8.0, r.min.y), pos2(r.max.x - 52.0, r.max.y));
|
||||
let mut child = ui.new_child(egui::UiBuilder::new().max_rect(text_r));
|
||||
// The variable bound to this paint's colour (its key on the layers: `fill` or `fills/<i>/color`).
|
||||
let bound = app.session.doc().ok().and_then(|d| {
|
||||
let bound = app.session.doc().ok().filter(|_| !region).and_then(|d| {
|
||||
let key = ids.first().and_then(|id| d.node(*id)).and_then(|n| figcraft_engine::cmd::lists::paint_binding_key(n, fills, i))?;
|
||||
Some((key.to_string(), crate::variable_ui::bound_name(d, &ids, key)?))
|
||||
});
|
||||
@@ -502,7 +506,7 @@ fn paint_row(app: &mut FigApp, ui: &mut Ui, ids: &[NodeId], i: usize, p: &Paint,
|
||||
_ => {
|
||||
child.painter().text(pos2(text_r.min.x, r.center().y), Align2::LEFT_CENTER, paint_label(p), regular(11.0), t.text);
|
||||
// One layer's image fill: crop it on the canvas.
|
||||
if fills && ids.len() == 1 && matches!(p.kind, PaintKind::Image(_)) {
|
||||
if fills && ids.len() == 1 && !region && matches!(p.kind, PaintKind::Image(_)) {
|
||||
let cr = Rect::from_center_size(pos2(text_r.max.x - 9.0, r.center().y), vec2(18.0, 18.0));
|
||||
let cresp = ui.interact(cr, Id::unique(("crop_image", i)), Sense::click()).tip(tr("Crop image"));
|
||||
icons::paint(ui.painter(), cr.shrink(3.0), Icon::Crop, if cresp.hovered() { t.icon } else { t.icon_secondary });
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
//! The Fill section with a region of a vector selected in edit mode (#1875), driven through egui
|
||||
//! headless: the region's own fill shows, and what belongs to the layer (its fill style, a
|
||||
//! variable bound to its fill) stays off the region's rows.
|
||||
|
||||
use egui::accesskit::Role;
|
||||
use egui::{Pos2, pos2};
|
||||
use figcraft_doc::{NodeId, NodeKind, Region, Segment, VectorNetwork, Vertex};
|
||||
use figcraft_engine::Session;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::layers_drag_tests::Ui;
|
||||
|
||||
/// Four points and four straight segments of a square, appended to `n`.
|
||||
fn square(n: &mut VectorNetwork, x0: f64, y0: f64, size: f64) {
|
||||
let v0 = n.vertices.len();
|
||||
for (x, y) in [(x0, y0), (x0 + size, y0), (x0 + size, y0 + size), (x0, y0 + size)] {
|
||||
n.vertices.push(Vertex::new(x, y));
|
||||
}
|
||||
for k in 0..4 {
|
||||
n.segments.push(Segment::line(v0 + k, v0 + (k + 1) % 4));
|
||||
}
|
||||
}
|
||||
|
||||
/// A vector of two squares, the outer one's region then the inner one's, its fill bound to a
|
||||
/// blue colour variable; in edit mode with the inner region selected and given a red fill of its
|
||||
/// own.
|
||||
fn ui() -> Ui {
|
||||
let mut s = Session::new();
|
||||
let r = s.execute("vector.create", &json!({"points": [[0, 0], [10, 0], [10, 10]], "closed": true})).unwrap();
|
||||
let id = NodeId(r["id"].as_u64().unwrap());
|
||||
{
|
||||
let doc = s.doc_mut().unwrap();
|
||||
let node = doc.get_mut(id).unwrap();
|
||||
let mut n = VectorNetwork::default();
|
||||
square(&mut n, 0.0, 0.0, 200.0);
|
||||
square(&mut n, 60.0, 60.0, 80.0);
|
||||
n.regions =
|
||||
vec![Region { loops: vec![vec![0, 1, 2, 3]], ..Default::default() }, Region { loops: vec![vec![4, 5, 6, 7]], ..Default::default() }];
|
||||
node.kind = NodeKind::Vector { network: n };
|
||||
node.transform = figcraft_geom::Affine::IDENTITY;
|
||||
node.fills = vec![figcraft_color::Paint::solid(figcraft_color::Color::DEFAULT_SHAPE)];
|
||||
figcraft_engine::vectoredit::refit(doc, id).unwrap();
|
||||
}
|
||||
s.execute("variable.createCollection", &json!({"name": "Theme"})).unwrap();
|
||||
s.execute("variable.create", &json!({"collection": "Theme", "name": "brand", "type": "color", "value": "#0000FF"})).unwrap();
|
||||
s.execute("variable.bind", &json!({"ids": [id.0], "field": "fill", "variable": "brand"})).unwrap();
|
||||
s.execute("select.set", &json!({"ids": [id.0]})).unwrap();
|
||||
s.execute("vector.edit.begin", &json!({"id": id.0})).unwrap();
|
||||
s.execute("vector.edit.select", &json!({"regions": [1]})).unwrap();
|
||||
s.execute("fill.set", &json!({"ids": [id.0], "color": "#FF0000"})).unwrap();
|
||||
let mut u = Ui::new(s);
|
||||
u.ctx.enable_accesskit();
|
||||
u.frame(vec![]);
|
||||
u
|
||||
}
|
||||
|
||||
/// The accessible controls after a frame: role, label, value and centre.
|
||||
fn controls(u: &mut Ui) -> Vec<(Role, String, String, Pos2)> {
|
||||
u.frame(vec![]);
|
||||
let out = u.frame_output(vec![]);
|
||||
let nodes = out.platform_output.accesskit_update.expect("an AccessKit update").nodes;
|
||||
nodes
|
||||
.iter()
|
||||
.map(|(_, n)| {
|
||||
let at = n.bounds().map_or(pos2(0.0, 0.0), |b| pos2(((b.x0 + b.x1) / 2.0) as f32, ((b.y0 + b.y1) / 2.0) as f32));
|
||||
(n.role(), n.label().unwrap_or_default().to_string(), n.value().unwrap_or_default().to_string(), at)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn button(c: &[(Role, String, String, Pos2)], name: &str) -> Pos2 {
|
||||
c.iter().find(|(r, l, _, _)| *r == Role::Button && l == name).map(|(_, _, _, p)| *p).unwrap_or_else(|| panic!("no {name} button: {c:?}"))
|
||||
}
|
||||
|
||||
/// What the Fill section shows, between its + and the Stroke section's: its hex rows, whether a
|
||||
/// Detach variable button is there, and whether its header has a Styles button.
|
||||
fn fill_section(c: &[(Role, String, String, Pos2)]) -> (Vec<String>, bool, bool) {
|
||||
let (top, bottom) = (button(c, "Add fill"), button(c, "Add stroke"));
|
||||
let inside = |p: &Pos2| p.y > top.y + 2.0 && p.y < bottom.y - 2.0;
|
||||
let rows = c.iter().filter(|(r, _, _, p)| *r == Role::TextInput && inside(p)).map(|(_, _, v, _)| v.clone()).collect();
|
||||
let detach = c.iter().any(|(_, l, _, p)| l == "Detach variable" && inside(p));
|
||||
let styles = c.iter().any(|(r, l, _, p)| *r == Role::Button && l == "Styles" && (p.y - top.y).abs() < 2.0);
|
||||
(rows, detach, styles)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_selected_regions_rows_leave_out_the_layers_style_and_variable() {
|
||||
let mut u = ui();
|
||||
let red = figcraft_color::Color::from_hex("#FF0000").unwrap().to_hex();
|
||||
let (rows, detach, styles) = fill_section(&controls(&mut u));
|
||||
assert_eq!(rows, [red], "the region's own red, as a hex field");
|
||||
assert!(!detach, "the layer's variable binding is not the region's");
|
||||
assert!(!styles, "no fill style for a region's fills");
|
||||
// No region selected: the layer's row again, with its bound variable and the Styles button.
|
||||
u.app.run("vector.edit.select", json!({}));
|
||||
let (rows, detach, styles) = fill_section(&controls(&mut u));
|
||||
assert!(rows.is_empty(), "the layer's paint shows as its variable, not a hex field: {rows:?}");
|
||||
assert!(detach && styles);
|
||||
}
|
||||
+2
-1
@@ -726,7 +726,8 @@ from the pen's open end; clicking a point connects or branches), `vector.edit.pe
|
||||
point; with `points`, the areas under the rest go the way the first went, like a drag), `fill.set`,
|
||||
`fill.add`, `fill.remove`, `fill.move` and `fill.paste` on the edited layer while regions are
|
||||
selected (`vector.edit.select {regions}`), which edit those regions' own fills and leave the
|
||||
layer's (`regions: [index, ...]` names them on any vector layer),
|
||||
layer's (`regions: [index, ...]` names them on any vector layer; `fill.copy` copies the selected
|
||||
regions' paint),
|
||||
`vector.edit.cut {segment, t} | {vertex} | {from: [x, y], to: [x, y]}` (the last cuts every path the
|
||||
page line crosses; parts that come apart move to layers of their own above, listed in `layers`),
|
||||
`vector.edit.erase {points, radius?}` (the eraser),
|
||||
|
||||
+1
-1
@@ -86,7 +86,7 @@ interaction feel not yet compared.
|
||||
| Rectangle, ellipse (arc handles and Design panel arc fields), polygon, star, line, arrow; Shift/Alt | done | Alt draws lines from the centre too (`shapes_behaviour_tests`) |
|
||||
| Corner radius handles (inside each corner of a rectangle or frame, a polygon's or star's top point; Alt: one corner), star ratio handle; Count and Ratio fields | done | handles hide under 48 px on screen; no count handle on the canvas (the Count field instead); corner smoothing rounds rectangles and frames only |
|
||||
| Pen: vector networks, branch, close, bend; pencil | done | pen pressure on Windows and from browser styluses only; outside edit mode the Pen closes on its first point, connects to its other points and continues a selected open path from an end point (`vector_multi_behaviour_tests`); Escape ends the path and keeps its layer in edit mode with the pen, so the next path joins the same vector and another Escape leaves edit mode (#760, `pen_end_tests`); Enter or a double-click ends the path (at the double-click's point, added once) and edits the vector with the Move tool, a double-click with the idle pen edits the layer under it, and in edit mode the pen's Enter or double-click takes the Move tool (#1430, `pen_vector_edit_tests`); switching from Design to Prototype or Inspect ends the path and returns to Move (#308) |
|
||||
| Vector edit mode: bend, paint bucket, lasso, cut, eraser, transform box, point snapping, mirroring | done | a double-click a few pixels off a thin stroke enters edit mode (#646); entering edit mode and picking a region keep the canvas still (#322); a point dropped on another point merges with it, and dropping one end of an open path on the other closes it (a 2-point line's end just moves, #401); Flatten keeps open paths open (#377); paint bucket drags across areas; cut drags divide a shape into layers; delete and heal follows the curve; rounded points are drawn; Shift-click deselects a point at a corner of the box (`vector_behaviour_tests`); several vectors edited at once, Shift-click handles to drag them together, a cap and join per point, rounded points beside curves (`vector_multi_behaviour_tests`); filled regions with the Move tool: the area under the pointer is hatched ("/" lines in the selection colour, clipped by the area's own fill rule, holes left out, gone 3.5 px from a point or 2 px from a segment at any zoom), the line spacing follows octave folding (fitted to measurements at several zoom levels: 8 px at every power-of-two zoom, growing with the zoom and stepping back at √2 · 2ⁿ) and stays fixed to the shape when panning; a click selects the area and its points (moved with it but drawn unselected), Shift adds or removes an area, a press on a selected area drags the whole selection, no hover hatch during drags, a click on an empty spot (a hole) clears it and stays in edit mode; `vector.edit.select {regions}` (`regions_tests`, `vector_regions_tests`); with regions selected, the Fill section (its rows and the colour picker) and `fill.set`, `fill.add`, `fill.remove`, `fill.move` and `fill.paste` edit those regions' own fills, the layer's untouched, or the regions named by `regions: [index, ...]` (#1875). Not yet: areas of other layers edited at once |
|
||||
| Vector edit mode: bend, paint bucket, lasso, cut, eraser, transform box, point snapping, mirroring | done | a double-click a few pixels off a thin stroke enters edit mode (#646); entering edit mode and picking a region keep the canvas still (#322); a point dropped on another point merges with it, and dropping one end of an open path on the other closes it (a 2-point line's end just moves, #401); Flatten keeps open paths open (#377); paint bucket drags across areas; cut drags divide a shape into layers; delete and heal follows the curve; rounded points are drawn; Shift-click deselects a point at a corner of the box (`vector_behaviour_tests`); several vectors edited at once, Shift-click handles to drag them together, a cap and join per point, rounded points beside curves (`vector_multi_behaviour_tests`); filled regions with the Move tool: the area under the pointer is hatched ("/" lines in the selection colour, clipped by the area's own fill rule, holes left out, gone 3.5 px from a point or 2 px from a segment at any zoom), the line spacing follows octave folding (fitted to measurements at several zoom levels: 8 px at every power-of-two zoom, growing with the zoom and stepping back at √2 · 2ⁿ) and stays fixed to the shape when panning; a click selects the area and its points (moved with it but drawn unselected), Shift adds or removes an area, a press on a selected area drags the whole selection, no hover hatch during drags, a click on an empty spot (a hole) clears it and stays in edit mode; `vector.edit.select {regions}` (`regions_tests`, `vector_regions_tests`); with regions selected, the Fill section (its rows and the colour picker) and `fill.set`, `fill.add`, `fill.remove`, `fill.move` and `fill.paste` edit those regions' own fills, the layer's untouched, or the regions named by `regions: [index, ...]`; Copy fill takes the region's paint, and what belongs to the layer (its fill style, a variable bound to its fill, the image crop) stays off a region's rows (#1875). Not yet: areas of other layers edited at once |
|
||||
| Shape builder (Shift+M) on fills, strokes and open paths; variable width (Shift+W), one side with Alt | done | |
|
||||
| Boolean groups: clicked where the result is, double-click or Cmd/Ctrl-click into operands, live results while dragging or editing points, per-operand operations | done | a group past the path operations' caps (250 000 segments, 2 million possible crossings, 4 million segments swept over a fold) draws without its result, with a warning, and Flatten refuses it (#1104) |
|
||||
| Text: point, auto height, fixed; caret, words, lines, IME | done | Backspace or Delete at the start of a list item takes its marker and keeps its level, the paragraph starting where the item's text did ("delete the counter, but keep the same level of indentation", Create bulleted and numbered lists; `text_tail_tests::list_keys_at_list_items`, text crate `behaviour_tests`); max lines on auto-width text keep it auto width, hugging up to that many lines and growing with its longest line ("Text resizing is set to auto height or auto width", Explore text properties; `text_behaviour_tests::max_lines_on_auto_width_text_keeps_auto_width_and_hugs_that_many_lines`); Alt+Enter while editing opens the link at the caret, web and mail links only (`text.openLink`; "press ⌥Option Return/Alt Enter to activate a focused link", Accessibility at Figma; `text_link_tests`); the font picker previews a family on the canvas while hovered (no undo step; Escape restores, #653) and Up/Down step through it (#352); a selection highlight after a line break starts at that line's start and an empty last line shows its mark (#665); carets step over whole emoji and accented letters; colour emoji draw from their colour tables (#300); IME composition replayed in tests as macOS sends it, not yet checked on a Mac (#747); CJK words by script run (no dictionary); vertical trim in Type settings; no spell check |
|
||||
|
||||
Reference in new issue
Block a user