Merge #740: engine: fill rules as vector.fillRule and vector.reverseLoop (@yelintong)

This commit is contained in:
bflatastic
2026-10-24 09:23:50 +07:00
15 changed files with 489 additions and 0 deletions
+157
View File
@@ -107,6 +107,20 @@ pub fn specs() -> Vec<CommandSpec> {
}
ok()
}),
spec!(
"vector.fillRule",
"Set fill rule",
"{ids? | id?, region? (a filled area of a vector; every area when left out), rule: nonZero|evenOdd}",
true,
fill_rule
),
spec!(
"vector.reverseLoop",
"Reverse loop",
"{ids? | id?, region, loop} (a loop of a filled area; its direction flips, which changes how the area fills)",
true,
reverse_loop
),
spec!("vector.simplify", "Simplify vector", "{ids?, tolerance?}", true, simplify),
spec!("vector.offset", "Offset path", "{ids?, offset}", true, offset_path),
spec!("vector.join", "Join selection", "{ids?} (in vector edit mode: joins the selected points)", true, |s, p| {
@@ -729,3 +743,146 @@ fn resize_to_fit(s: &mut Session, p: &Value) -> Result<Value> {
}
Ok(json!({"ids": fitted}))
}
/// The vector network of `id`, when it is a vector layer.
fn network_mut(doc: &mut Document, id: NodeId) -> Option<&mut VectorNetwork> {
match &mut doc.get_mut(id).ok()?.kind {
NodeKind::Vector { network } => Some(network),
_ => None,
}
}
/// The id a fill rule is written as.
fn rule_id(rule: WindingRule) -> &'static str {
match rule {
WindingRule::NonZero => "nonZero",
WindingRule::EvenOdd => "evenOdd",
}
}
/// `region` as an index: absent (or null) means every area.
fn area_index(cmd: &str, p: &Value) -> Result<Option<usize>> {
match get(p, "region") {
None | Some(Value::Null) => Ok(None),
Some(v) => {
let k = f64_of(v).ok_or_else(|| bad(cmd, "`region` must be a number"))?;
if !(0.0..1e9).contains(&k) || k.fract() != 0.0 {
return Err(bad(cmd, "`region` out of range"));
}
Ok(Some(k as usize))
}
}
}
/// Segments another one- or two-segment loop lists: a short loop takes its direction from the ends
/// its segments are stored with, so re-storing one of those would turn that loop round too. A loop
/// of three or more segments is left out: its ring is walked in list order, which the stored ends
/// don't decide.
fn short_loop_segments(net: &VectorNetwork, skip: (usize, usize)) -> std::collections::HashSet<usize> {
let mut out = std::collections::HashSet::new();
for (r, region) in net.regions.iter().enumerate() {
for (l, lp) in region.loops.iter().enumerate() {
if (r, l) != skip && lp.len() <= 2 {
out.extend(lp.iter().copied());
}
}
}
out
}
fn fill_rule(s: &mut Session, p: &Value) -> Result<Value> {
let ids = req_targets("vector.fillRule", s, p)?;
let rule = match string(p, "rule").as_deref().map(str::trim) {
Some("evenOdd" | "evenodd" | "EVENODD") => WindingRule::EvenOdd,
Some("nonZero" | "nonzero" | "NONZERO") => WindingRule::NonZero,
_ => return Err(bad("vector.fillRule", "`rule` is nonZero or evenOdd")),
};
let only = area_index("vector.fillRule", p)?;
let doc = s.doc_mut()?;
let mut count = 0usize;
for id in ids {
let Some(net) = network_mut(doc, id) else { continue };
match only {
Some(r) => {
if let Some(region) = net.regions.get_mut(r) {
region.winding = rule;
count += 1;
}
}
None => {
for region in &mut net.regions {
region.winding = rule;
count += 1;
}
}
}
}
if count == 0 {
return Err(EngineError::Disabled("vector.fillRule".into(), "no filled area to set (select a vector)".into()));
}
Ok(json!({"regions": count, "rule": rule_id(rule)}))
}
/// One loop of a vector, worked out but not yet written back: its segments in the new order and,
/// for a loop of one or two segments, the one to store the other way.
struct LoopFlip {
id: NodeId,
turned: Vec<usize>,
store: Option<usize>,
}
/// Turn one loop of a filled area round: its segments are walked the other way, so the area it
/// bounds counts the other way in the winding (which turns a hole into a filled part and back).
///
/// How a loop is walked depends on how many segments it has:
/// - three or more: the order they are listed in, each segment turned to continue from the point
/// reached. Reversing the list is therefore enough, and the stored ends are left alone (a shared
/// segment's ends belong to its neighbour too);
/// - two: the same ring either way round, so one of the two has to be stored the other way instead;
/// - one: only its stored ends say which way it runs, so they are swapped.
///
/// A short loop can only turn round by re-storing a segment, which is refused when another short
/// loop lists that segment too: it would turn that area round as well.
fn reverse_loop(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "vector.reverseLoop";
let ids = req_targets(cmd, s, p)?;
let region = req_index(cmd, p, "region")?;
let index = req_index(cmd, p, "loop")?;
let doc = s.doc_mut()?;
// Work every loop out before touching the document: one that can't turn round refuses the call
// and the others stay as they were.
let mut flips: Vec<LoopFlip> = Vec::new();
for id in ids {
let Some(NodeKind::Vector { network }) = doc.node(id).map(|n| &n.kind) else { continue };
let Some(loop_) = network.regions.get(region).and_then(|r| r.loops.get(index)) else { continue };
if loop_.is_empty() {
continue;
}
let mut store = None;
if loop_.len() <= 2 {
let busy = short_loop_segments(network, (region, index));
let Some(pick) = loop_.iter().copied().filter(|s| !busy.contains(s)).min() else {
return Err(EngineError::Disabled(
cmd.into(),
"the loop shares its segments with another one- or two-segment loop, so turning it round would turn that one round too".into(),
));
};
store = Some(pick);
}
flips.push(LoopFlip { id, turned: loop_.iter().rev().copied().collect(), store });
}
if flips.is_empty() {
return Err(EngineError::Disabled(cmd.into(), "no such loop (select a vector with filled areas)".into()));
}
for LoopFlip { id, turned, store } in flips {
let Some(net) = network_mut(doc, id) else { continue };
if let Some(seg) = store.and_then(|i| net.segments.get_mut(i)) {
std::mem::swap(&mut seg.a, &mut seg.b);
std::mem::swap(&mut seg.ta, &mut seg.tb);
}
if let Some(l) = net.regions.get_mut(region).and_then(|r| r.loops.get_mut(index)) {
*l = turned;
}
}
Ok(json!({"reversed": true}))
}
+2
View File
@@ -1905,6 +1905,8 @@ mod variants_discovery_tests;
#[cfg(test)]
mod vector_behaviour_tests;
#[cfg(test)]
mod vector_fill_rule_tests;
#[cfg(test)]
mod vector_multi_behaviour_tests;
#[cfg(test)]
mod vector_offset_tests;
+306
View File
@@ -0,0 +1,306 @@
//! Fill rules on vectors: `vector.fillRule` sets the rule a filled area fills by and
//! `vector.reverseLoop` turns one of its loops round — which is how a hole is made and unmade.
//! The numbers come from the two rules' definitions: non-zero fills where the winding count is not
//! zero (so a hole is a loop running against its outer loop), even-odd where it is odd (so the
//! direction of a loop doesn't matter).
use figcraft_doc::{NodeId, NodeKind, Region, Segment, VectorNetwork, Vertex, WindingRule};
use figcraft_geom::{BezPath, Point};
use serde_json::{Value, json};
use crate::Session;
fn ex(s: &mut Session, cmd: &str, p: Value) -> Value {
s.execute(cmd, &p).unwrap_or_else(|e| panic!("{cmd}: {e}"))
}
fn net(s: &Session, id: NodeId) -> VectorNetwork {
match &s.doc().unwrap().node(id).unwrap().kind {
NodeKind::Vector { network } => network.clone(),
other => panic!("not a vector: {other:?}"),
}
}
/// A 100 × 100 square with a 25…75 hole, both loops in one area: the outer one clockwise, the inner
/// one the same way round (so non-zero fills the hole too until it is turned round).
fn ring() -> VectorNetwork {
let mut path = BezPath::new();
for (x0, y0, x1, y1) in [(0.0, 0.0, 100.0, 100.0), (25.0, 25.0, 75.0, 75.0)] {
path.move_to((x0, y0));
path.line_to((x1, y0));
path.line_to((x1, y1));
path.line_to((x0, y1));
path.close_path();
}
let mut net = VectorNetwork::from_path(&path);
net.regions[0].winding = WindingRule::NonZero;
net
}
/// A selected vector layer holding `network`.
fn vector_layer(s: &mut Session, network: VectorNetwork) -> NodeId {
let id = s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 100, "height": 100})).unwrap()["id"].as_u64().unwrap();
let id = NodeId(id);
ex(s, "vector.edit.begin", json!({"id": id}));
ex(s, "vector.edit.end", json!({}));
{
let doc = s.doc_mut().unwrap();
match &mut doc.get_mut(id).unwrap().kind {
NodeKind::Vector { network: net } => *net = network,
other => panic!("not a vector: {other:?}"),
}
}
ex(s, "select.set", json!({"ids": [id]}));
id
}
/// A selected vector layer holding `ring()`.
fn ringed(s: &mut Session) -> NodeId {
vector_layer(s, ring())
}
/// Two 50 × 100 squares side by side share the vertical edge between them: one segment listed by
/// both areas' single loops. A segment shared between areas is the case `vector.reverseLoop` must
/// not flip, or a neighbour's fill or stroke would turn round with it.
fn two_squares() -> VectorNetwork {
// 0:A(0,0) 1:B(50,0) 2:C(100,0) 3:D(100,100) 4:E(50,100) 5:F(0,100)
// s0 A→B (left bottom), s1 B→E (the shared vertical edge), s2 E→F (left top),
// s3 F→A (left left), s4 B→C (right bottom), s5 C→D (right right), s6 D→E (right top);
// the right area's left edge is s1 walked the other way, so the squares share one segment.
// region 0: left square A→B→E→F→A = [s0, s1, s2, s3].
// region 1: right square B→C→D→E→B = [s4, s5, s6, s1] (s1 walked backwards).
VectorNetwork {
vertices: vec![
Vertex::new(0.0, 0.0),
Vertex::new(50.0, 0.0),
Vertex::new(100.0, 0.0),
Vertex::new(100.0, 100.0),
Vertex::new(50.0, 100.0),
Vertex::new(0.0, 100.0),
],
segments: vec![
Segment::line(0, 1),
Segment::line(1, 4),
Segment::line(4, 5),
Segment::line(5, 0),
Segment::line(1, 2),
Segment::line(2, 3),
Segment::line(3, 4),
],
regions: vec![
Region { loops: vec![vec![0, 1, 2, 3]], winding: WindingRule::NonZero },
Region { loops: vec![vec![4, 5, 6, 1]], winding: WindingRule::NonZero },
],
}
}
/// A selected vector layer holding [`two_squares`].
fn squared(s: &mut Session) -> NodeId {
vector_layer(s, two_squares())
}
/// Two areas of two segments each over the same line: one curve above it and one below. Both areas
/// list the line's segment — the first area first (`[line, above]`), the second area last
/// (`[below, line]`) — and a two-segment ring reads the same whichever way its list is turned, so
/// these only turn round when a segment is stored the other way.
fn two_lenses() -> VectorNetwork {
// 0:A(0,0) 1:B(100,0)
// s0 A→B the straight line both areas use; s1 B→A curving above it; s2 A→B curving below it.
VectorNetwork {
vertices: vec![Vertex::new(0.0, 0.0), Vertex::new(100.0, 0.0)],
segments: vec![
Segment::line(0, 1),
Segment { a: 1, b: 0, ta: (0.0, -50.0), tb: (0.0, -50.0) },
Segment { a: 0, b: 1, ta: (0.0, 50.0), tb: (0.0, 50.0) },
],
regions: vec![
Region { loops: vec![vec![0, 1]], winding: WindingRule::NonZero },
Region { loops: vec![vec![2, 0]], winding: WindingRule::NonZero },
],
}
}
/// A selected vector layer holding [`two_lenses`].
fn lensed(s: &mut Session) -> NodeId {
vector_layer(s, two_lenses())
}
/// Two areas whose only loop is the same closed segment (a curve the path closes with a straight
/// line): a one-segment loop's direction is its segment's stored ends, and both areas read them.
fn one_shared_segment() -> VectorNetwork {
VectorNetwork {
vertices: vec![Vertex::new(0.0, 0.0), Vertex::new(100.0, 0.0)],
segments: vec![Segment { a: 0, b: 1, ta: (0.0, -60.0), tb: (0.0, -60.0) }],
regions: vec![Region { loops: vec![vec![0]], winding: WindingRule::NonZero }, Region { loops: vec![vec![0]], winding: WindingRule::NonZero }],
}
}
/// The point in the middle of the hole, and one on the square's left band.
const HOLE: Point = Point::new(50.0, 50.0);
const BAND: Point = Point::new(10.0, 50.0);
#[test]
fn a_loop_running_with_its_outer_loop_fills_the_hole_until_it_is_turned_round() {
let mut s = Session::new();
let id = ringed(&mut s);
let n = net(&s, id);
assert_eq!(n.regions.len(), 1);
assert_eq!(n.regions[0].loops.len(), 2, "an outer loop and the hole's");
assert!(n.region_contains(0, BAND), "the band fills");
assert!(n.region_contains(0, HOLE), "the hole fills while its loop runs the same way");
// Turning the inner loop round makes it a hole under non-zero.
let r = ex(&mut s, "vector.reverseLoop", json!({"id": id, "region": 0, "loop": 1}));
assert_eq!(r["reversed"], json!(true));
let n = net(&s, id);
assert!(n.region_contains(0, BAND), "the band still fills");
assert!(!n.region_contains(0, HOLE), "the hole is empty");
assert_eq!(n.region_winding(0, HOLE), Some(0), "its loops cancel out there");
// Undo turns it back.
s.undo().unwrap();
assert!(net(&s, id).region_contains(0, HOLE), "one undo step restores the loop");
}
#[test]
fn the_rule_switches_every_area_or_only_the_one_asked_for() {
let mut s = Session::new();
let id = ringed(&mut s);
// Even-odd leaves the hole empty whatever way its loop runs.
let r = ex(&mut s, "vector.fillRule", json!({"id": id, "rule": "evenOdd"}));
assert_eq!((r["regions"].clone(), r["rule"].clone()), (json!(1), json!("evenOdd")));
let n = net(&s, id);
assert_eq!(n.regions[0].winding, WindingRule::EvenOdd);
assert!(!n.region_contains(0, HOLE), "even-odd counts the hole's loop as a second crossing");
// And back.
ex(&mut s, "vector.fillRule", json!({"id": id, "rule": "nonZero"}));
assert!(net(&s, id).region_contains(0, HOLE), "non-zero fills it again");
// One area of several, by index.
let n = net(&s, id);
let mut two = n.clone();
two.regions.push(n.regions[0].clone());
{
let doc = s.doc_mut().unwrap();
match &mut doc.get_mut(id).unwrap().kind {
NodeKind::Vector { network } => *network = two,
other => panic!("not a vector: {other:?}"),
}
}
ex(&mut s, "vector.fillRule", json!({"id": id, "region": 1, "rule": "evenOdd"}));
let n = net(&s, id);
assert_eq!(n.regions[0].winding, WindingRule::NonZero, "the other area keeps its rule");
assert_eq!(n.regions[1].winding, WindingRule::EvenOdd);
// Every area at once.
ex(&mut s, "vector.fillRule", json!({"id": id, "rule": "evenOdd"}));
assert!(net(&s, id).regions.iter().all(|r| r.winding == WindingRule::EvenOdd));
}
#[test]
fn junk_parameters_are_refused_and_change_nothing() {
let mut s = Session::new();
let id = ringed(&mut s);
let before = net(&s, id);
for p in [
json!({"id": id, "rule": "nope"}),
json!({"id": id}),
json!({"id": id, "rule": 7}),
json!({"id": id, "rule": "evenOdd", "region": -1}),
json!({"id": id, "rule": "evenOdd", "region": 1.5}),
json!({"id": id, "rule": "evenOdd", "region": "x"}),
json!({"id": id, "rule": "evenOdd", "region": 99}),
] {
assert!(s.execute("vector.fillRule", &p).is_err(), "{p} is refused");
}
for p in [
json!({"id": id}),
json!({"id": id, "region": 0}),
json!({"id": id, "region": 0, "loop": 99}),
json!({"id": id, "region": 99, "loop": 0}),
json!({"id": id, "region": -2, "loop": 0}),
] {
assert!(s.execute("vector.reverseLoop", &p).is_err(), "{p} is refused");
}
assert_eq!(net(&s, id), before, "nothing changed");
// A rectangle that was never a vector is refused too.
let rect = s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap()["id"].as_u64().unwrap();
assert!(s.execute("vector.fillRule", &json!({"id": rect, "rule": "evenOdd"})).is_err());
assert!(s.execute("vector.reverseLoop", &json!({"id": rect, "region": 0, "loop": 0})).is_err());
}
#[test]
fn reversing_a_loop_shared_with_a_neighbour_leaves_its_segment_and_fill_alone() {
let mut s = Session::new();
let id = squared(&mut s);
let n = net(&s, id);
assert_eq!(n.regions.len(), 2, "two areas, side by side");
// Both areas fill under non-zero: a point in each.
assert!(n.region_contains(0, Point::new(25.0, 50.0)), "the left area fills");
assert!(n.region_contains(1, Point::new(75.0, 50.0)), "the right area fills");
// The vertical edge between them is one segment listed by both areas' loops.
const SHARED: usize = 1;
assert!(n.regions[0].loops[0].contains(&SHARED) && n.regions[1].loops[0].contains(&SHARED), "one segment is shared");
let before = n.segments[SHARED];
let left_winding = n.region_winding(0, Point::new(25.0, 50.0)).unwrap();
assert_ne!(left_winding, 0, "the left area winds once");
// Reverse the left area's only loop.
let r = ex(&mut s, "vector.reverseLoop", json!({"id": id, "region": 0, "loop": 0}));
assert_eq!(r["reversed"], json!(true));
let n = net(&s, id);
// The shared segment's stored ends are untouched: reversing the loop must not flip a neighbour's
// segment (reversing the list already turns this loop's traversal of it the other way).
assert_eq!(n.segments[SHARED], before, "the shared segment is left as it was");
// The neighbour's fill is unchanged: it still fills its interior.
assert!(n.region_contains(1, Point::new(75.0, 50.0)), "the neighbour still fills");
// The reversed loop really turned round: its winding at a left-area point flipped sign.
assert_eq!(n.region_winding(0, Point::new(25.0, 50.0)), Some(-left_winding), "the left loop reversed");
// Undo restores the shared segment and the loop.
s.undo().unwrap();
assert_eq!(net(&s, id).segments[SHARED], before, "undo restores the shared segment too");
}
#[test]
fn a_two_segment_loop_turns_round_with_its_shared_segment_listed_first_or_last() {
let mut s = Session::new();
let id = lensed(&mut s);
const SHARED: usize = 0;
const ABOVE: Point = Point::new(50.0, -25.0);
const BELOW: Point = Point::new(50.0, 25.0);
let n = net(&s, id);
assert_eq!(n.regions.len(), 2, "two areas over the same line");
assert_eq!((n.regions[0].loops[0][0], n.regions[0].loops[0].len()), (SHARED, 2), "the shared segment is listed first");
assert_eq!((n.regions[1].loops[0][1], n.regions[1].loops[0].len()), (SHARED, 2), "and last");
assert!(n.region_contains(0, ABOVE), "the area above the line fills");
assert!(n.region_contains(1, BELOW), "the area below it fills");
let (above, below) = (n.region_winding(0, ABOVE).unwrap(), n.region_winding(1, BELOW).unwrap());
assert_ne!(above, 0, "the area above winds once");
assert_ne!(below, 0, "the area below winds once");
let shared = n.segments[SHARED];
// Turn the area that lists the shared segment first round: a two-segment ring reads the same
// either way, so this only works by storing one of its segments the other way.
assert_eq!(ex(&mut s, "vector.reverseLoop", json!({"id": id, "region": 0, "loop": 0}))["reversed"], json!(true));
let n = net(&s, id);
assert_eq!(n.segments[SHARED], shared, "the shared segment keeps its ends");
assert!(n.region_contains(0, ABOVE), "the area still fills");
assert_eq!(n.region_winding(0, ABOVE), Some(-above), "it really turned round");
assert_eq!(n.region_winding(1, BELOW), Some(below), "the neighbour did not");
// And the area that lists it last.
ex(&mut s, "vector.reverseLoop", json!({"id": id, "region": 1, "loop": 0}));
let n = net(&s, id);
assert_eq!(n.segments[SHARED], shared, "still untouched");
assert_eq!(n.region_winding(1, BELOW), Some(-below), "the second area really turned round");
assert_eq!(n.region_winding(0, ABOVE), Some(-above), "the first stays as it was");
// Undo turns each back.
s.undo().unwrap();
assert_eq!(net(&s, id).region_winding(1, BELOW), Some(below), "undo turns the second back");
s.undo().unwrap();
assert_eq!(net(&s, id).region_winding(0, ABOVE), Some(above), "and the first");
}
#[test]
fn a_one_segment_loop_shared_with_another_is_refused_instead_of_claiming_a_turn() {
let mut s = Session::new();
let id = vector_layer(&mut s, one_shared_segment());
let before = net(&s, id);
let err = s.execute("vector.reverseLoop", &json!({"id": id, "region": 0, "loop": 0})).unwrap_err();
let message = err.to_string();
assert!(message.contains("shares its segments"), "the error says why: {message}");
assert_eq!(net(&s, id), before, "nothing changed");
}
+2
View File
@@ -2242,11 +2242,13 @@ status = "draft — needs native review"
"vector.edit.tool" = "Nástroj úprav vektoru"
"vector.edit.transform" = "Transformovat body"
"vector.edit.widthPoint" = "Bod šířky"
"vector.fillRule" = "Nastavit pravidlo výplně"
"vector.flatten" = "Sloučit do vektoru"
"vector.join" = "Spojit výběr"
"vector.offset" = "Odsadit cestu"
"vector.outlineStroke" = "Převést tah na obrysy"
"vector.reverse" = "Obrátit směr cesty"
"vector.reverseLoop" = "Obrátit smyčku"
"vector.simplify" = "Zjednodušit vektor"
"vector.smoothJoin" = "Hladce spojit výběr"
"version.autosave" = "Automaticky uložit verzi"
+2
View File
@@ -2194,11 +2194,13 @@ status = "draft — needs native review"
"vector.edit.tool" = "Vektorbearbeitungswerkzeug"
"vector.edit.transform" = "Punkte transformieren"
"vector.edit.widthPoint" = "Breitenpunkt"
"vector.fillRule" = "Füllregel festlegen"
"vector.flatten" = "Zusammenfügen"
"vector.join" = "Auswahl verbinden"
"vector.offset" = "Pfad versetzen"
"vector.outlineStroke" = "Kontur in Pfad umwandeln"
"vector.reverse" = "Pfadrichtung umkehren"
"vector.reverseLoop" = "Schleife umdrehen"
"vector.simplify" = "Vektor vereinfachen"
"vector.smoothJoin" = "Auswahl glatt verbinden"
"version.autosave" = "Version automatisch speichern"
+2
View File
@@ -2314,11 +2314,13 @@ status = "draft — needs native review"
"vector.edit.tool" = "Herramienta de edición vectorial"
"vector.edit.transform" = "Transformar puntos"
"vector.edit.widthPoint" = "Punto de ancho"
"vector.fillRule" = "Establecer regla de relleno"
"vector.flatten" = "Acoplar"
"vector.join" = "Unir selección"
"vector.offset" = "Desplazar trazado"
"vector.outlineStroke" = "Contornear trazo"
"vector.reverse" = "Invertir dirección del trazado"
"vector.reverseLoop" = "Invertir bucle"
"vector.simplify" = "Simplificar vector"
"vector.smoothJoin" = "Unir selección con suavizado"
"version.autosave" = "Guardar versión automáticamente"
+2
View File
@@ -2291,11 +2291,13 @@ status = "draft — needs native review"
"vector.edit.tool" = "Outil de modification vectorielle"
"vector.edit.transform" = "Transformer les points"
"vector.edit.widthPoint" = "Point de largeur"
"vector.fillRule" = "Définir la règle de remplissage"
"vector.flatten" = "Aplatir"
"vector.join" = "Joindre la sélection"
"vector.offset" = "Décaler le tracé"
"vector.outlineStroke" = "Convertir le contour en tracé"
"vector.reverse" = "Inverser le sens du tracé"
"vector.reverseLoop" = "Inverser la boucle"
"vector.simplify" = "Simplifier le vecteur"
"vector.smoothJoin" = "Joindre la sélection en lissant"
"version.autosave" = "Enregistrer automatiquement une version"
+2
View File
@@ -2246,11 +2246,13 @@ status = "draft — needs native review"
"vector.edit.tool" = "Alat edit vector"
"vector.edit.transform" = "Transformasi titik"
"vector.edit.widthPoint" = "Titik lebar"
"vector.fillRule" = "Atur aturan isian"
"vector.flatten" = "Satukan"
"vector.join" = "Gabungkan pilihan"
"vector.offset" = "Offset path"
"vector.outlineStroke" = "Outline stroke"
"vector.reverse" = "Balik arah path"
"vector.reverseLoop" = "Balik loop"
"vector.simplify" = "Sederhanakan vector"
"vector.smoothJoin" = "Gabungkan pilihan dengan halus"
"version.autosave" = "Simpan versi otomatis"
+2
View File
@@ -2269,11 +2269,13 @@ status = "draft — needs native review"
"vector.edit.tool" = "ベクター編集ツール"
"vector.edit.transform" = "ポイントを変形"
"vector.edit.widthPoint" = "幅のポイント"
"vector.fillRule" = "塗りつぶし規則を設定"
"vector.flatten" = "平坦化"
"vector.join" = "選択範囲を連結"
"vector.offset" = "パスのオフセット"
"vector.outlineStroke" = "線のアウトライン化"
"vector.reverse" = "パスの方向を反転"
"vector.reverseLoop" = "ループを反転"
"vector.simplify" = "ベクターを単純化"
"vector.smoothJoin" = "選択範囲を滑らかに連結"
"version.autosave" = "自動保存バージョン"
+2
View File
@@ -2185,11 +2185,13 @@ status = "draft — needs native review"
"vector.edit.tool" = "벡터 편집 도구"
"vector.edit.transform" = "점 변형"
"vector.edit.widthPoint" = "너비 점"
"vector.fillRule" = "칠 규칙 설정"
"vector.flatten" = "평면화"
"vector.join" = "선택 항목 연결"
"vector.offset" = "패스 오프셋"
"vector.outlineStroke" = "선 윤곽선 만들기"
"vector.reverse" = "패스 방향 반전"
"vector.reverseLoop" = "루프 반전"
"vector.simplify" = "벡터 단순화"
"vector.smoothJoin" = "선택 항목 부드럽게 연결"
"version.autosave" = "자동 저장 버전"
+2
View File
@@ -2298,11 +2298,13 @@ status = "draft — needs native review"
"vector.edit.tool" = "Ferramenta de edição vetorial"
"vector.edit.transform" = "Transformar pontos"
"vector.edit.widthPoint" = "Ponto de largura"
"vector.fillRule" = "Definir regra de preenchimento"
"vector.flatten" = "Nivelar"
"vector.join" = "Unir seleção"
"vector.offset" = "Deslocar caminho"
"vector.outlineStroke" = "Contornar traço"
"vector.reverse" = "Inverter direção do caminho"
"vector.reverseLoop" = "Inverter laço"
"vector.simplify" = "Simplificar vetor"
"vector.smoothJoin" = "Unir seleção com suavização"
"version.autosave" = "Salvar versão automaticamente"
+2
View File
@@ -2305,11 +2305,13 @@ name = "Русский"
"vector.edit.tool" = "Инструмент редактирования вектора"
"vector.edit.transform" = "Трансформировать точки"
"vector.edit.widthPoint" = "Точка ширины"
"vector.fillRule" = "Задать правило заливки"
"vector.flatten" = "Свести"
"vector.join" = "Соединить выделение"
"vector.offset" = "Сместить путь"
"vector.outlineStroke" = "Преобразовать обводку в контур"
"vector.reverse" = "Обратить направление пути"
"vector.reverseLoop" = "Развернуть контур"
"vector.simplify" = "Упростить вектор"
"vector.smoothJoin" = "Плавно соединить выделение"
"version.autosave" = "Автосохранить версию"
+2
View File
@@ -2269,11 +2269,13 @@ name = "Svenska"
"vector.edit.tool" = "Vektorredigeringsverktyg"
"vector.edit.transform" = "Transformera punkter"
"vector.edit.widthPoint" = "Breddpunkt"
"vector.fillRule" = "Ange fyllnadsregel"
"vector.flatten" = "Platta till"
"vector.join" = "Sammanfoga markering"
"vector.offset" = "Förskjut bana"
"vector.outlineStroke" = "Konvertera kontur till form"
"vector.reverse" = "Vänd banans riktning"
"vector.reverseLoop" = "Vänd slinga"
"vector.simplify" = "Förenkla vektor"
"vector.smoothJoin" = "Sammanfoga markering mjukt"
"version.autosave" = "Autospara version"
+2
View File
@@ -2246,11 +2246,13 @@ name = "Türkçe"
"vector.edit.tool" = "Vektör düzenleme aracı"
"vector.edit.transform" = "Noktaları dönüştür"
"vector.edit.widthPoint" = "Genişlik noktası"
"vector.fillRule" = "Dolgu kuralını ayarla"
"vector.flatten" = "Düzleştir"
"vector.join" = "Seçimi birleştir"
"vector.offset" = "Yolu ötele"
"vector.outlineStroke" = "Konturu ana hatlara dönüştür"
"vector.reverse" = "Yol yönünü ters çevir"
"vector.reverseLoop" = "Döngüyü çevir"
"vector.simplify" = "Vektörü sadeleştir"
"vector.smoothJoin" = "Seçimi yumuşak birleştir"
"version.autosave" = "Otomatik kayıt sürümü"
+2
View File
@@ -2186,11 +2186,13 @@ status = "draft — needs native review"
"vector.edit.tool" = "矢量编辑工具"
"vector.edit.transform" = "变换点"
"vector.edit.widthPoint" = "宽度点"
"vector.fillRule" = "设置填充规则"
"vector.flatten" = "拼合"
"vector.join" = "连接所选项"
"vector.offset" = "偏移路径"
"vector.outlineStroke" = "轮廓化描边"
"vector.reverse" = "反转路径方向"
"vector.reverseLoop" = "反转环"
"vector.simplify" = "简化矢量"
"vector.smoothJoin" = "平滑连接所选项"
"version.autosave" = "自动保存版本"