Merge pull request #2526 from vangel76/feat/2523-shape-builder-gap-detection

Shape Builder: Gap Detection and Tool Options (#2523)
This commit is contained in:
BFlat
2026-10-22 03:03:45 +07:00
committed by GitHub
36 changed files with 351 additions and 24 deletions
+11 -5
View File
@@ -39,7 +39,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Shape Builder",
[],
None,
"{ids?|id?: [id…] (default: selection), points: [[x,y]…] (drag path; one point = click), erase?: bool (erase mode, Alt: deletes the regions touched, and the pieces of open paths that cut regions and the edges (outline pieces between the points where paths meet; see shapeBuilder.regions) within tolerance; a deleted edge opens its path there, and edges bounding a deleted region go with it), tolerance?: pt (2, erase only), openFilledAsClosed?: true (Consider Open Filled Paths as Closed: a filled open path bounds a region as if an invisible edge closed it; false: it cuts the regions it crosses like an unfilled one), pickColorFrom?: \"artwork\" (default: the merged shape takes the fill of the front-most object over the first region the drag touches that an object covers; the current fill when only lines enclose every region touched) | \"swatches\" (the current fill), fill?: \"#rrggbb\" | {color|swatch|gradient|none} (overrides pickColorFrom)} → {ids, merged, regions, lines, edges} (counts deleted or merged)",
"{ids?|id?: [id…] (default: selection), points: [[x,y]…] (drag path; one point = click), erase?: bool (erase mode, Alt: deletes the regions touched, and the pieces of open paths that cut regions and the edges (outline pieces between the points where paths meet; see shapeBuilder.regions) within tolerance; a deleted edge opens its path there, and edges bounding a deleted region go with it), tolerance?: pt (2, erase only), gap?: pt (0–72; 0: Gap Detection off), openFilledAsClosed?: true (Consider Open Filled Paths as Closed: a filled open path bounds a region as if an invisible edge closed it; false: it cuts the regions it crosses like an unfilled one), pickColorFrom?: \"artwork\" (default: the merged shape takes the fill of the front-most object over the first region the drag touches that an object covers; the current fill when only lines enclose every region touched) | \"swatches\" (the current fill), fill?: \"#rrggbb\" | {color|swatch|gradient|none} (overrides pickColorFrom)} → {ids, merged, regions, lines, edges} (counts deleted or merged)",
has_selection_or_ids,
sb_merge
),
@@ -48,7 +48,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Shape Builder Regions",
[],
None,
"{ids?|id?, openFilledAsClosed?: true (as shapeBuilder.merge)} → {regions: [{bounds: [x0,y0,x1,y1], area, sources: [id…]}], lines: [{bounds, source: id}], edges: [{bounds, source: id}]} faces of the planar arrangement (unfilled open paths cut them, and filled ones too with openFilledAsClosed false; sources is empty for an area only they enclose), the pieces of those open paths and the edges (the other paths' outlines cut where paths meet; source: the front-most path along it), which erasing deletes",
"{ids?|id?, gap?: pt (0–72; 0: Gap Detection off), openFilledAsClosed?: true (as shapeBuilder.merge)} → {regions: [{bounds: [x0,y0,x1,y1], area, sources: [id…]}], lines: [{bounds, source: id}], edges: [{bounds, source: id}]} faces of the planar arrangement (unfilled open paths cut them, and filled ones too with openFilledAsClosed false; sources is empty for an area only they enclose), the pieces of those open paths and the edges (the other paths' outlines cut where paths meet; source: the front-most path along it), which erasing deletes",
has_selection_or_ids,
sb_regions
),
@@ -236,6 +236,12 @@ fn fill_param(s: &Session, p: &Value, cmd: &str) -> Result<Option<Paint>> {
}
}
/// Gap Detection length in points, bounded like Live Paint's custom gap size.
fn gap_param(p: &Value) -> f64 {
let gap = f64_or(p, "gap", 0.0);
if gap.is_finite() { gap.clamp(0.0, po::SHAPE_BUILDER_MAX_GAP) } else { 0.0 }
}
fn sb_merge(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "shapeBuilder.merge";
let pts = points_param(p, "points").ok_or_else(|| bad(C, "missing points [[x,y]…]"))?;
@@ -262,7 +268,7 @@ fn sb_merge(s: &mut Session, p: &Value) -> Result<Value> {
let roots = root_ids(s, p)?;
let label = if erase { "Shape Builder Delete" } else { "Shape Builder" };
let (ids, merged, touched) = s.edit(label, |d, sel| {
let (map, leaves) = BuilderMap::new(d, &roots, erase, open_filled_as_closed);
let (map, leaves) = BuilderMap::with_gap(d, &roots, erase, open_filled_as_closed, gap_param(p));
let leaves: Vec<Node> = leaves.into_iter().cloned().collect();
if map.shapes.is_empty() {
return Err(EngineError::Other("Shape Builder: select paths or compound paths".into()));
@@ -280,7 +286,7 @@ fn sb_merge(s: &mut Session, p: &Value) -> Result<Value> {
// Edges come back from the planar map within a few hundredths of the outlines they follow.
let cut_tol = step * 0.2;
let picked: Vec<&po::Region> = touched.regions.iter().filter_map(|&i| map.regions.get(i).map(|r| &r.0)).collect();
let faces = po::FaceMerger::new(&map.shapes);
let faces = po::FaceMerger::with_gap(&map.shapes, gap_param(p));
let union = if picked.is_empty() { PathData::default() } else { faces.merge(&picked) };
// The merged shape takes the look of the front-most object over the first region, or (for an
// area only lines enclose) of the front-most object and the current fill.
@@ -378,7 +384,7 @@ fn replace_roots(d: &mut Document, roots: &[NodeId], nodes: Vec<Node>) -> Result
fn sb_regions(s: &mut Session, p: &Value) -> Result<Value> {
let roots = root_ids(s, p)?;
let d = &s.doc()?.doc;
let (map, leaves) = BuilderMap::new(d, &roots, true, bool_or(p, "openFilledAsClosed", true));
let (map, leaves) = BuilderMap::with_gap(d, &roots, true, bool_or(p, "openFilledAsClosed", true), gap_param(p));
let id_of = |i: usize| leaves.get(i).map(|n| n.id.0);
let regions: Vec<Value> = map
.regions
+49
View File
@@ -358,6 +358,55 @@ fn shape_builder_fills_an_area_lines_enclose() {
assert_eq!(ids(&r).len(), 4, "the triangle + the three lines, which only bound it");
}
/// Two diagonals and a crossbar that stops about 3 pt short of each: the triangle they bound.
fn lines_with_gaps(s: &mut Session) {
let l1 = line(s, (0.0, 0.0), (100.0, 100.0));
let l2 = line(s, (100.0, 0.0), (0.0, 100.0));
let l3 = line(s, (24.0, 80.0), (76.0, 80.0));
s.execute("select.set", &json!({"ids": [l1.0, l2.0, l3.0]})).unwrap();
}
#[test]
fn shape_builder_gap_detection_closes_small_gaps_between_lines() {
let mut s = session();
lines_with_gaps(&mut s);
let regions = |s: &mut Session, p: Value| s.execute("shapeBuilder.regions", &p).unwrap()["regions"].as_array().unwrap().len();
assert_eq!(regions(&mut s, json!({})), 0, "off by default");
assert_eq!(regions(&mut s, json!({"gap": 2})), 0, "the gap is longer");
assert_eq!(regions(&mut s, json!({"gap": 3})), 1);
assert!(s.execute("shapeBuilder.merge", &json!({"points": [[50, 70]]})).is_err(), "no gap, no region");
let r = s.execute("shapeBuilder.merge", &json!({"points": [[50, 70]], "gap": 3})).unwrap();
let merged = NodeId(r["merged"].as_u64().unwrap());
let a = area(&s, merged);
assert!(a > 850.0 && a < 900.0, "the triangle and its two bridges: {a}");
assert_eq!(ids(&r).len(), 4, "the bridges are no art: the merged shape and the three lines");
assert_eq!(open_paths(&s, &ids(&r)).len(), 3);
}
#[test]
fn shape_builder_tool_gap_detection_option() {
let mut s = session();
lines_with_gaps(&mut s);
let v = ViewInfo::default();
s.select_tool("shapeBuilder", v).unwrap();
assert_eq!((s.tool_options()["gapDetection"].clone(), s.tool_options()["gapLength"].clone()), (json!(false), json!(3.0)));
let click = |s: &mut Session| {
s.pointer(&PointerEvent::new(PointerKind::Down, 50.0, 70.0), v).unwrap();
s.pointer(&PointerEvent::new(PointerKind::Up, 50.0, 70.0), v).unwrap();
};
click(&mut s);
assert_eq!(layer_children(&s), 3, "Gap Detection is off");
s.set_tool_option_cmd(&json!({"values": {"gapDetection": true, "gapLength": 6.0}})).unwrap();
click(&mut s);
assert_eq!(layer_children(&s), 4, "the triangle");
// The options are kept when another tool is chosen.
s.select_tool("selection", v).unwrap();
assert_eq!(
(s.tool_options_of("shapeBuilder")["gapDetection"].clone(), s.tool_options_of("shapeBuilder")["gapLength"].clone()),
(json!(true), json!(6.0))
);
}
#[test]
fn shape_builder_closes_filled_open_paths() {
let mut s = session();
+53
View File
@@ -489,6 +489,21 @@ fn structured_junk() {
("symbol.options", json!({"name": "Dot", "nineSlice": true, "nineSliceGuides": [1e308, -1e308, 0, 0]})),
("symbol.options", json!({"nineSlice": 7, "nineSliceGuides": []})),
("symbol.options", json!({"nineSlice": false, "nineSliceGuides": null})),
// Shape Builder Gap Detection: the `gap` param and the tool options.
("shapeBuilder.merge", json!({"ids": [2, 3], "points": [[100, 80]], "gap": 1e308})),
("shapeBuilder.merge", json!({"ids": [2, 3], "points": [[100, 80], [120, 90]], "gap": -1e308})),
("shapeBuilder.merge", json!({"ids": [2, 3], "points": [[100, 80]], "gap": f64::MAX})),
("shapeBuilder.merge", json!({"ids": [2, 3], "points": [[100, 80]], "gap": "NaN"})),
("shapeBuilder.merge", json!({"ids": [2, 3], "points": [[100, 80]], "gap": "1e999"})),
("shapeBuilder.merge", json!({"ids": [2, 3], "points": [[100, 80]], "gap": null})),
("shapeBuilder.merge", json!({"points": [[100, 80]], "gap": [1, 2]})),
("shapeBuilder.merge", json!({"points": [[100, 80]], "gap": {"a": 1}, "erase": true})),
("shapeBuilder.merge", json!({"points": [[100, 80]], "gap": 1e-300, "erase": true, "tolerance": 1e308})),
("shapeBuilder.regions", json!({"gap": 1e308})),
("shapeBuilder.regions", json!({"ids": [0, 1, 2, 3], "gap": -1})),
("shapeBuilder.regions", json!({"ids": [2, 3], "gap": "x"})),
("shapeBuilder.regions", json!({"gap": true})),
("shapeBuilder.regions", json!({"gap": 12, "ids": [u64::MAX]})),
];
let mut failures = vec![];
for fx in Fixture::ALL {
@@ -501,6 +516,44 @@ fn structured_junk() {
assert!(failures.is_empty(), "{} failures:\n{}", failures.len(), failures.join("\n"));
}
/// Shape Builder Tool Options (Gap Detection) with junk values: never a panic, and the tool still
/// answers with options and works on a drag afterwards.
#[test]
fn shape_builder_options_junk() {
let cases = [
json!({"values": {"gapDetection": true, "gapLength": 1e308}}),
json!({"values": {"gapDetection": "yes", "gapLength": -1e308}}),
json!({"values": {"gapDetection": null, "gapLength": "NaN"}}),
json!({"values": {"gapDetection": [true], "gapLength": [1, 2]}}),
json!({"values": {"gapDetection": true, "gapLength": "1e999"}}),
json!({"values": {"gapLength": {"a": 1}, "bogus": 1}}),
json!({"key": "gapLength", "value": f64::MAX}),
json!({"key": "gapLength", "value": f64::MIN_POSITIVE}),
json!({"key": "gapDetection"}),
json!({"key": "gapDetection", "value": 1}),
json!({"values": [1, 2]}),
json!({"values": "x"}),
];
let mut failures = vec![];
for fx in Fixture::ALL {
for p in &cases {
let mut s = fx.session();
let r = catch_quiet(|| {
let _ = s.select_tool("shapeBuilder", Default::default());
let _ = s.set_tool_option_cmd(p);
let _ = s.set_tool_option_cmd(&json!({"values": {"gapDetection": true}}));
no_panic(s.execute("shapeBuilder.regions", &json!({})))
});
if let Err(msg) = r {
failures.push(format!("PANIC {p} [{fx:?}]: {msg}"));
} else if let Err(e) = check_session(&s) {
failures.push(format!("{p} [{fx:?}]: {e}"));
}
}
}
assert!(failures.is_empty(), "{} failures:\n{}", failures.len(), failures.join("\n"));
}
/// Area Type Options with junk fit values, on selected area type (the fixtures have none), with
/// text that overflows: never a panic, the document stays sound, and editing still works.
#[test]
+2 -2
View File
@@ -21,8 +21,8 @@ pub use edit::{
pub use offset::{Cap, Join, offset_path, outline_stroke, stroke_region};
pub use pathfinder::{FaceMerger, PathfinderOp, Region, Shape, merge_regions, pathfinder, region_at, regions};
pub use planar::{
BuilderArrangement, SHAPE_BUILDER_MAX_SEGMENTS, cut_out, encloses_area, find_gaps, has_open_path, interior_point, live_paint,
live_paint_with_gaps, shape_builder,
BuilderArrangement, SHAPE_BUILDER_MAX_GAP, SHAPE_BUILDER_MAX_SEGMENTS, cut_out, encloses_area, find_gaps, has_open_path, interior_point,
live_paint, live_paint_with_gaps, shape_builder, shape_builder_with_gap,
};
/// Errors from fallible operations.
+6
View File
@@ -351,6 +351,12 @@ impl FaceMerger {
Self { arr: if has_open { None } else { arrangement(shapes) }, shapes: shapes.len(), lines, snap: std::cell::OnceCell::new() }
}
/// Merge faces found with Gap Detection using the same temporary bridges as Live Paint.
/// Source indices stay those of the original inputs; bridges have no fill and are never art.
pub fn with_gap(shapes: &[Shape], gap: f64) -> Self {
Self::new(&crate::planar::gap_inputs(shapes, gap))
}
/// The union of `faces`.
pub fn merge(&self, faces: &[&Region]) -> PathData {
self.exact(faces).or_else(|| self.planar(faces)).filter(|p| !p.is_empty()).unwrap_or_else(|| merge_regions(faces))
+105 -1
View File
@@ -613,12 +613,44 @@ pub fn has_open_path(shapes: &[Shape]) -> bool {
/// [`SHAPE_BUILDER_MAX_SEGMENTS`] segments or a map that can't be built, only the filled areas
/// count and there are no pieces.
pub fn shape_builder(shapes: &[Shape], edges: bool) -> BuilderArrangement {
shape_builder_with_gap(shapes, edges, 0.0)
}
/// Most distance (document units) a Shape Builder gap may span.
pub const SHAPE_BUILDER_MAX_GAP: f64 = 72.0;
/// Original inputs and the shared detector's temporary, unfilled bridges. Keep the originals'
/// order and keys so only their split pieces can be returned as art.
pub(crate) fn gap_inputs(shapes: &[Shape], gap: f64) -> Vec<Shape> {
let mut closed = shapes.to_vec();
let segments: usize = shapes.iter().flat_map(|s| &s.path.subpaths).map(|sp| sp.segment_count()).sum();
if segments <= SHAPE_BUILDER_MAX_SEGMENTS && gap.is_finite() && gap > 0.0 && has_open_path(shapes) {
let gaps = find_gaps(shapes, gap.min(SHAPE_BUILDER_MAX_GAP));
if !gaps.is_empty() {
let path = PathData::new(gaps.iter().map(|g| SubPath::polyline(g, false)).collect());
closed.push(Shape::new(path, FillRule::NonZero, u64::MAX));
}
}
closed
}
/// [`shape_builder`] with Gap Detection: open paths whose ends come within `gap` (document units)
/// of another path are bridged for the regions they bound, so a region left open by a small gap is
/// found. A `gap` of 0 is [`shape_builder`]. The bridges are in no region's sources and in no piece
/// of `lines` or `edges`.
pub fn shape_builder_with_gap(shapes: &[Shape], edges: bool, gap: f64) -> BuilderArrangement {
let has_open = has_open_path(shapes);
let segments: usize = shapes.iter().flat_map(|s| &s.path.subpaths).map(|sp| sp.segment_count()).sum();
let planar = (segments <= SHAPE_BUILDER_MAX_SEGMENTS)
.then(|| {
let keyed: Vec<Shape> = shapes.iter().enumerate().map(|(i, s)| Shape::new(s.path.clone(), s.rule, i as u64)).collect();
live_paint(&keyed)
if has_open && gap.is_finite() && gap > 0.0 {
let (faces, pieces) = live_paint(&gap_inputs(&keyed, gap));
// Gap targets split original outlines too, but the bridges themselves aren't art.
(faces, pieces.into_iter().filter(|p| p.key < shapes.len() as u64).collect())
} else {
live_paint(&keyed)
}
})
.filter(|(faces, pieces)| !faces.is_empty() || !pieces.is_empty());
let Some((faces, pieces)) = planar else {
@@ -1542,6 +1574,78 @@ mod tests {
assert_eq!(lines.len(), 9);
}
#[test]
fn shape_builder_merges_adjacent_gap_closed_faces_without_keeping_bridges() {
let outline = Shape::new(
PathData::single(SubPath::polyline(
&[Point::new(0.0, 4.0), Point::new(0.0, 100.0), Point::new(100.0, 100.0), Point::new(100.0, 0.0), Point::new(0.0, 0.0)],
false,
)),
FillRule::NonZero,
0,
);
let shapes = [outline, line((50.0, 0.0), (50.0, 100.0), 1)];
let arr = shape_builder_with_gap(&shapes, true, 6.0);
assert_eq!(arr.regions.len(), 2);
let faces: Vec<_> = arr.regions.iter().collect();
let merged = crate::FaceMerger::with_gap(&shapes, 6.0).merge(&faces);
assert!((crate::area(&merged, FillRule::NonZero).abs() - 10000.0).abs() < 1e-6);
assert_eq!(merged.subpaths.len(), 1, "the shared edge disappears");
assert!(arr.lines.iter().chain(&arr.edges).all(|p| p.key < 2), "bridges are not art");
// A request over 72 pt cannot close a gap of 80 pt.
let large = [Shape::new(
PathData::single(SubPath::polyline(
&[Point::new(0.0, 80.0), Point::new(0.0, 200.0), Point::new(200.0, 200.0), Point::new(200.0, 0.0), Point::new(0.0, 0.0)],
false,
)),
FillRule::NonZero,
0,
)];
assert!(shape_builder_with_gap(&large, false, 1000.0).regions.is_empty());
}
#[test]
fn shape_builder_gap_detection_closes_small_gaps() {
// Three sides of a 100 x 100 square, the fourth open by 4: no region without Gap Detection.
let open = |gap: f64| {
let pts = [(0.0, gap), (0.0, 100.0), (100.0, 100.0), (100.0, 0.0), (0.0, 0.0)];
let pts: Vec<Point> = pts.iter().map(|&(x, y)| Point::new(x, y)).collect();
vec![Shape::new(PathData::single(SubPath::polyline(&pts, false)), FillRule::NonZero, 0)]
};
let s = open(4.0);
assert!(shape_builder(&s, false).regions.is_empty());
assert!(shape_builder_with_gap(&s, false, 3.0).regions.is_empty(), "a gap longer than the length stays open");
let arr = shape_builder_with_gap(&s, true, 6.0);
assert_eq!(arr.regions.len(), 1);
assert!((area(&arr.regions[0]) - 10000.0).abs() < 1e-6);
// The bridge is no art: the path comes back as it was, in its own pieces only.
assert!(arr.lines.iter().chain(&arr.edges).all(|p| p.key == 0));
let len: f64 = arr.lines.iter().map(|p| p.path.to_bezpath().perimeter(1e-6)).sum();
assert!((len - 396.0).abs() < 1e-6, "{len}");
// Two paths whose ends miss each other: a U and a lid 3 above it.
let u = Shape::new(
PathData::single(SubPath::polyline(
&[Point::new(0.0, 0.0), Point::new(0.0, 100.0), Point::new(100.0, 100.0), Point::new(100.0, 0.0)],
false,
)),
FillRule::NonZero,
0,
);
let s = [u, line((0.0, -3.0), (100.0, -3.0), 1)];
assert!(shape_builder_with_gap(&s, false, 2.0).regions.is_empty());
let arr = shape_builder_with_gap(&s, false, 3.5);
assert_eq!(arr.regions.len(), 1);
assert!((area(&arr.regions[0]) - 10300.0).abs() < 1e-6);
// An end close to the side of another path bridges to the nearest point on it.
let s = [rect(0.0, 0.0, 100.0, 100.0, 0), line((50.0, 140.0), (50.0, 104.0), 1)];
assert_eq!(shape_builder_with_gap(&s, false, 5.0).regions.len(), 1, "nothing is enclosed, nothing breaks");
// Bad gaps are no gap.
let s = open(4.0);
for gap in [f64::NAN, f64::INFINITY, -3.0] {
assert!(shape_builder_with_gap(&s, false, gap).regions.is_empty());
}
}
#[test]
fn shape_builder_closed_only_or_degenerate_is_the_filled_arrangement() {
let s = [rect(0.0, 0.0, 100.0, 100.0, 0), rect(50.0, 0.0, 150.0, 100.0, 1)];
+41 -4
View File
@@ -253,11 +253,16 @@ impl BuilderMap {
/// `open_filled_as_closed` (Consider Open Filled Paths as Closed), a filled open path bounds a
/// region as if an invisible edge closed it; without it, the path cuts like a line.
pub fn new<'a>(doc: &'a Document, roots: &[NodeId], with_edges: bool, open_filled_as_closed: bool) -> (Self, Vec<&'a Node>) {
Self::with_gap(doc, roots, with_edges, open_filled_as_closed, 0.0)
}
/// The same arrangement with Gap Detection, using the shared Live Paint gap finder.
pub fn with_gap<'a>(doc: &'a Document, roots: &[NodeId], with_edges: bool, open_filled_as_closed: bool, gap: f64) -> (Self, Vec<&'a Node>) {
let (shapes, leaves) = shape_builder_shapes(doc, roots, open_filled_as_closed);
if shapes.is_empty() {
return (Self::default(), leaves);
}
let arr = po::shape_builder(&shapes, with_edges);
let arr = po::shape_builder_with_gap(&shapes, with_edges, gap);
let bounded = |pieces: Vec<po::Shape>| {
pieces
.into_iter()
@@ -442,6 +447,8 @@ struct RegionCache {
live: bool,
/// The map has the Shape Builder's outline edges.
edges: bool,
/// Gap Detection distance; changing tool options invalidates the cache.
gap: f64,
}
impl RegionCache {
@@ -459,7 +466,7 @@ impl RegionCache {
let regions = if shapes.is_empty() { vec![] } else { faces_of(po::live_paint(&shapes).0) };
BuilderMap { regions, ..Default::default() }
} else {
BuilderMap::new(cx.doc, &roots, edges, open_filled_as_closed).0
BuilderMap::with_gap(cx.doc, &roots, edges, open_filled_as_closed, self.gap).0
};
self.key = Some(key);
self.edges = edges;
@@ -473,6 +480,10 @@ impl RegionCache {
// ---------- Shape Builder ----------
pub use po::SHAPE_BUILDER_MAX_GAP;
/// Small, Medium and Large gap lengths in points.
pub const GAP_LENGTHS: [f64; 3] = [3.0, 6.0, 12.0];
/// Shape Builder Tool Options (double-click the tool; `tool.setOption`), kept across tool switches
/// and saved with the preferences.
#[derive(Clone, Debug, PartialEq)]
@@ -496,11 +507,23 @@ pub struct ShapeBuilderOptions {
/// Highlight Fill (`highlightFill`, on): in merge mode, the regions under the pointer or
/// touched by the drag are shaded with a gray mesh. When it is off, they are not highlighted.
pub highlight_fill: bool,
/// Close gaps up to `gap_length` while finding regions.
pub gap_detection: bool,
/// Gap Length in points (at most [`SHAPE_BUILDER_MAX_GAP`]).
pub gap_length: f64,
}
impl Default for ShapeBuilderOptions {
fn default() -> Self {
Self { open_filled_as_closed: true, swatches: false, cursor_swatch_preview: false, straight_line: false, highlight_fill: true }
Self {
open_filled_as_closed: true,
swatches: false,
cursor_swatch_preview: false,
straight_line: false,
highlight_fill: true,
gap_detection: false,
gap_length: GAP_LENGTHS[0],
}
}
}
@@ -560,6 +583,10 @@ pub struct ShapeBuilderTool {
const LINE_TOL_PX: f64 = 4.0;
impl ShapeBuilderTool {
fn gap(&self) -> f64 {
if self.opts.gap_detection { self.opts.gap_length } else { 0.0 }
}
fn line_tol(cx: &ToolContext, alt: bool) -> Option<f64> {
alt.then(|| cx.tol(LINE_TOL_PX))
}
@@ -665,7 +692,7 @@ impl Tool for ShapeBuilderTool {
let pts: Vec<Value> = points.iter().map(|q| json!([q.x, q.y])).collect();
// The mode the drag started in, which its highlight showed.
let erase = self.alt;
let mut params = json!({ "ids": json_ids(&roots), "points": pts, "erase": erase });
let mut params = json!({ "ids": json_ids(&roots), "points": pts, "erase": erase, "gap": self.gap() });
if let Some(t) = Self::line_tol(cx, erase) {
params["tolerance"] = json!(t);
}
@@ -761,6 +788,8 @@ impl Tool for ShapeBuilderTool {
"cursorSwatchPreview": o.cursor_swatch_preview,
"selection": if o.straight_line { "straightLine" } else { "freeform" },
"highlightFill": o.highlight_fill,
"gapDetection": o.gap_detection,
"gapLength": o.gap_length,
})
}
@@ -772,8 +801,16 @@ impl Tool for ShapeBuilderTool {
"cursorSwatchPreview" => o.cursor_swatch_preview = v.as_bool().unwrap_or(o.cursor_swatch_preview),
"selection" => o.straight_line = v.as_str().map_or(o.straight_line, |s| s == "straightLine"),
"highlightFill" => o.highlight_fill = v.as_bool().unwrap_or(o.highlight_fill),
"gapDetection" => o.gap_detection = v.as_bool().unwrap_or(o.gap_detection),
"gapLength" => {
if let Some(n) = v.as_f64().filter(|n| n.is_finite()) {
o.gap_length = n.clamp(0.0, SHAPE_BUILDER_MAX_GAP);
}
}
_ => {}
}
self.cache.key = None;
self.cache.gap = self.gap();
}
}
+5 -1
View File
@@ -42,7 +42,11 @@ const ROWS: &[Row] = &[
(&["star"], None, &["points"]),
(&["freeTransform"], None, &["constrain"]),
// Shape Builder Tool Options (`builder::ShapeBuilderOptions`).
(&["shapeBuilder"], None, &["openFilledAsClosed", "pickColorFrom", "cursorSwatchPreview", "selection", "highlightFill"]),
(
&["shapeBuilder"],
None,
&["openFilledAsClosed", "pickColorFrom", "cursorSwatchPreview", "selection", "highlightFill", "gapDetection", "gapLength"],
),
(&["artboard"], None, &["moveArt", "scaleArt"]),
// Flare Tool Options (`extra::FLARE_OPTIONS` and the Rays and Rings checkboxes).
(
@@ -18,7 +18,7 @@ pub const KIND: &str = "shapeBuilderOptions";
pub(super) const SPEC: DialogSpec = DialogSpec { heading, body, confirm, min_width: 360.0, ..DialogSpec::FORM };
/// The options the dialog edits.
const KEYS: [&str; 5] = ["openFilledAsClosed", "pickColorFrom", "cursorSwatchPreview", "selection", "highlightFill"];
const KEYS: [&str; 7] = ["openFilledAsClosed", "pickColorFrom", "cursorSwatchPreview", "selection", "highlightFill", "gapDetection", "gapLength"];
/// Width of the label column.
const LABEL_W: f32 = 110.0;
@@ -37,6 +37,24 @@ pub fn open(app: &mut VectorcraftApp) -> Value {
}
fn body(_app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
form::check(ui, d, "gapDetection", tl!("Gap Detection"));
ui.add_enabled_ui(d.bool("gapDetection"), |ui| {
widgets::label_row(ui, tl!("Gap Length:"), LABEL_W, |ui| {
let lengths = vectorcraft_tools::builder::GAP_LENGTHS;
let len = d.f64("gapLength", lengths[0]);
let current = lengths.iter().position(|n| *n == len).unwrap_or(3);
let labels = [tl!("Small"), tl!("Medium"), tl!("Large"), tl!("Custom")];
if let Some(i) = widgets::dropdown(ui, "shape-builder-gap", labels[current], &labels, 120.0) {
d.fields.insert("gapLength".into(), json!(lengths.get(i).copied().unwrap_or(9.0)));
}
ui.add_enabled_ui(current == 3, |ui| {
if let Some(n) = widgets::plain_field(ui, "shape-builder-gap-length", len, "pt", 1, 64.0) {
d.fields.insert("gapLength".into(), json!(n.clamp(0.0, vectorcraft_tools::builder::SHAPE_BUILDER_MAX_GAP)));
}
});
});
});
ui.add_space(8.0);
form::check(ui, d, "openFilledAsClosed", tl!("Consider Open Filled Paths as Closed"));
ui.add_space(8.0);
let sources = [("artwork", tl!("Artwork")), ("swatches", tl!("Color Swatches"))];
@@ -81,6 +99,22 @@ mod tests {
crate::tests_labels::painted_text(app, |app, ui| super::super::show(app, ui.ctx()))
}
#[test]
fn gap_options_share_the_existing_dialog_and_custom_is_capped() {
let mut a = app();
a.run("tool.options", json!({"tool": "shapeBuilder"})).unwrap();
let text = painted(&mut a);
assert!(text.contains("Gap Detection") && text.contains("Gap Length:"), "{text}");
let d = a.ui.dialog.as_mut().unwrap();
d.fields.insert("gapDetection".into(), json!(true));
d.fields.insert("gapLength".into(), json!(1000));
super::super::confirm(&mut a).unwrap();
let opts = a.session.tool_options_of("shapeBuilder");
assert_eq!(opts["gapLength"], 72.0);
assert_eq!(opts["gapDetection"], true);
assert_eq!(opts["openFilledAsClosed"], true);
}
#[test]
fn double_clicking_the_tool_opens_its_options_and_ok_keeps_them() {
let mut app = app();
@@ -124,7 +158,7 @@ mod tests {
let o = app.session.tool_options_of("shapeBuilder");
assert_eq!(
o,
json!({"openFilledAsClosed": false, "pickColorFrom": "swatches", "cursorSwatchPreview": true, "selection": "straightLine", "highlightFill": false})
json!({"openFilledAsClosed": false, "pickColorFrom": "swatches", "cursorSwatchPreview": true, "selection": "straightLine", "highlightFill": false, "gapDetection": false, "gapLength": 3.0})
);
app.run("tool.options", json!({"tool": "shapeBuilder"})).unwrap();
let d = app.ui.dialog.as_ref().unwrap();
+1
View File
@@ -5285,6 +5285,7 @@ theme Appearance Външен вид
Persistent Постоянна
Locate Library… Намиране на библиотека…
Library Panel Options Опции на панела с библиотека
Gap Length: Дължина на пропуска:
Artboard {n} of {total} Работна област {n} от {total}
Encoding and writing the file… Кодиране и записване на файла…
@plural {n} more export waiting|{n} more exports waiting Още {n} експортиране в изчакване|Още {n} експортирания в изчакване
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5328,6 +5328,7 @@ mark-style Japanese Japonské
@msg too many library panels were opened in this session v této relaci bylo otevřeno příliš mnoho panelů knihoven
@msg The image is {_1} × {_2} pixels, more than {LARGE_TRACE_SIDE} on a side: it is traced, but more slowly. A lower-fidelity preset or a smaller copy of the image traces faster. Obraz má {_1} × {_2} pixelů, na jedné straně více než {LARGE_TRACE_SIDE}: vektorizuje se, ale pomaleji. Rychleji se vektorizuje s předvolbou nižší věrnosti nebo menší kopie obrazu.
@msg The image was traced at {_1} × {_2} pixels, scaled down from {_3} × {_4} ({_5} megapixels at most). Obraz byl vektorizován v rozlišení {_1} × {_2} pixelů, zmenšený z {_3} × {_4} (nejvýše {_5} megapixelů).
Gap Length: Délka mezery:
Artboard {n} of {total} Pracovní plocha {n} z {total}
Encoding and writing the file… Kódování a zápis souboru…
@plural {n} more export waiting|{n} more exports waiting Čeká ještě {n} export|Čekají ještě {n} exporty|Čeká ještě {n} exportů
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5281,6 +5281,7 @@ swatch Bright Violet Kraftig violet
Persistent Vedvarende
Locate Library… Lokalisér bibliotek…
Library Panel Options Indstillinger for bibliotekspanel
Gap Length: Hullængde:
Artboard {n} of {total} Tegneflade {n} af {total}
Encoding and writing the file… Koder og skriver filen…
@plural {n} more export waiting|{n} more exports waiting {n} eksport mere venter|{n} eksporter mere venter
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4908,6 +4908,7 @@ effect Soft Weich
@msg too many library panels were opened in this session in dieser Sitzung wurden zu viele Bibliotheksbedienfelder geöffnet
@msg The image is {_1} × {_2} pixels, more than {LARGE_TRACE_SIDE} on a side: it is traced, but more slowly. A lower-fidelity preset or a smaller copy of the image traces faster. Das Bild hat {_1} × {_2} Pixel, mehr als {LARGE_TRACE_SIDE} auf einer Seite: Es wird nachgezeichnet, aber langsamer. Eine Vorgabe mit geringerer Detailtreue oder eine kleinere Kopie des Bildes geht schneller.
@msg The image was traced at {_1} × {_2} pixels, scaled down from {_3} × {_4} ({_5} megapixels at most). Das Bild wurde mit {_1} × {_2} Pixeln nachgezeichnet, verkleinert von {_3} × {_4} (höchstens {_5} Megapixel).
Gap Length: Lückenlänge:
Artboard {n} of {total} Zeichenfläche {n} von {total}
Encoding and writing the file… Datei wird codiert und geschrieben…
@plural {n} more export waiting|{n} more exports waiting {n} weiterer Export wartet|{n} weitere Exporte warten
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5280,6 +5280,7 @@ effect Points Σημεία
@msg too many library panels were opened in this session άνοιξαν πάρα πολλοί πίνακες βιβλιοθηκών σε αυτή τη συνεδρία
@msg The image is {_1} × {_2} pixels, more than {LARGE_TRACE_SIDE} on a side: it is traced, but more slowly. A lower-fidelity preset or a smaller copy of the image traces faster. Η εικόνα είναι {_1} × {_2} εικονοστοιχεία, πάνω από {LARGE_TRACE_SIDE} σε μία πλευρά: ανιχνεύεται, αλλά πιο αργά. Μια προρύθμιση χαμηλότερης πιστότητας ή ένα μικρότερο αντίγραφο της εικόνας ανιχνεύεται γρηγορότερα.
@msg The image was traced at {_1} × {_2} pixels, scaled down from {_3} × {_4} ({_5} megapixels at most). Η εικόνα ανιχνεύτηκε σε {_1} × {_2} εικονοστοιχεία, σμικρυμένη από {_3} × {_4} (το πολύ {_5} megapixel).
Gap Length: Μήκος κενού:
Artboard {n} of {total} Πίνακας σχεδίασης {n} από {total}
Encoding and writing the file… Κωδικοποίηση και εγγραφή του αρχείου…
@plural {n} more export waiting|{n} more exports waiting {n} ακόμη εξαγωγή σε αναμονή|{n} ακόμη εξαγωγές σε αναμονή
Can't render this file because it contains an unexpected character in line 2039 and column 19.
+1
View File
@@ -5270,6 +5270,7 @@ mark-style Japanese Japonesas
@msg too many library panels were opened in this session se abrieron demasiados paneles de biblioteca en esta sesión
@msg The image is {_1} × {_2} pixels, more than {LARGE_TRACE_SIDE} on a side: it is traced, but more slowly. A lower-fidelity preset or a smaller copy of the image traces faster. La imagen tiene {_1} × {_2} píxeles, más de {LARGE_TRACE_SIDE} en un lado: se calcará, pero más despacio. Un ajuste preestablecido de menor fidelidad o una copia más pequeña de la imagen se calcan más rápido.
@msg The image was traced at {_1} × {_2} pixels, scaled down from {_3} × {_4} ({_5} megapixels at most). La imagen se calcó a {_1} × {_2} píxeles, reducida desde {_3} × {_4} ({_5} megapíxeles como máximo).
Gap Length: Longitud del hueco:
Artboard {n} of {total} Mesa de trabajo {n} de {total}
Encoding and writing the file… Codificando y escribiendo el archivo…
@plural {n} more export waiting|{n} more exports waiting {n} exportación más en espera|{n} exportaciones más en espera
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5292,6 +5292,7 @@ mark-style Japanese Japanilainen
@msg too many library panels were opened in this session tässä istunnossa on avattu liian monta kirjastopaneelia
@msg The image is {_1} × {_2} pixels, more than {LARGE_TRACE_SIDE} on a side: it is traced, but more slowly. A lower-fidelity preset or a smaller copy of the image traces faster. Kuva on {_1} × {_2} kuvapistettä, yhdeltä sivultaan yli {LARGE_TRACE_SIDE}: se jäljitetään, mutta hitaammin. Pienemmän tarkkuuden esiasetus tai kuvan pienempi kopio jäljittyy nopeammin.
@msg The image was traced at {_1} × {_2} pixels, scaled down from {_3} × {_4} ({_5} megapixels at most). Kuva jäljitettiin kokoon {_1} × {_2} kuvapistettä pienennettynä koosta {_3} × {_4} (enintään {_5} megapikseliä).
Gap Length: Aukon pituus:
Artboard {n} of {total} Piirtoalusta {n}/{total}
Encoding and writing the file… Tiedostoa koodataan ja kirjoitetaan…
@plural {n} more export waiting|{n} more exports waiting {n} vienti odottaa|{n} vientiä odottaa
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5327,6 +5327,7 @@ theme Appearance Apparence
@msg too many library panels were opened in this session trop de panneaux de bibliothèque ont été ouverts dans cette session
@msg The image is {_1} × {_2} pixels, more than {LARGE_TRACE_SIDE} on a side: it is traced, but more slowly. A lower-fidelity preset or a smaller copy of the image traces faster. L’image fait {_1} × {_2} pixels, plus de {LARGE_TRACE_SIDE} sur un côté : elle est vectorisée, mais plus lentement. Un paramètre prédéfini moins fidèle ou une copie plus petite de l’image se vectorise plus vite.
@msg The image was traced at {_1} × {_2} pixels, scaled down from {_3} × {_4} ({_5} megapixels at most). L’image a été vectorisée à {_1} × {_2} pixels, réduite de {_3} × {_4} ({_5} mégapixels au plus).
Gap Length: Longueur de lacune :
Artboard {n} of {total} Plan de travail {n} sur {total}
Encoding and writing the file… Encodage et écriture du fichier…
@plural {n} more export waiting|{n} more exports waiting {n} autre exportation en attente|{n} autres exportations en attente
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5314,6 +5314,7 @@ imageTrace Outline Körvonal
@msg too many library panels were opened in this session ebben a munkamenetben túl sok könyvtárpanel nyílt meg
@msg The image is {_1} × {_2} pixels, more than {LARGE_TRACE_SIDE} on a side: it is traced, but more slowly. A lower-fidelity preset or a smaller copy of the image traces faster. A kép {_1} × {_2} képpont, egyik oldalán több mint {LARGE_TRACE_SIDE}: a vektorizálás megtörténik, de lassabban. Kisebb hűségű előbeállítással vagy a kép kisebb másolatával gyorsabb.
@msg The image was traced at {_1} × {_2} pixels, scaled down from {_3} × {_4} ({_5} megapixels at most). A kép vektorizálása {_1} × {_2} képponton történt, {_3} × {_4} méretről kicsinyítve (legfeljebb {_5} megapixel).
Gap Length: Rés hossza:
Artboard {n} of {total} Rajztábla: {n} / {total}
Encoding and writing the file… A fájl kódolása és írása…
@plural {n} more export waiting|{n} more exports waiting Még {n} exportálás várakozik
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5281,6 +5281,7 @@ swatch Bright Violet Violet cerah
Persistent Persisten
Locate Library… Temukan pustaka…
Library Panel Options Opsi panel pustaka
Gap Length: Panjang celah:
Artboard {n} of {total} Artboard {n} dari {total}
Encoding and writing the file… Mengodekan dan menulis berkas…
@plural {n} more export waiting|{n} more exports waiting {n} ekspor lagi menunggu
Can't render this file because it contains an unexpected character in line 3809 and column 19.
+1
View File
@@ -4942,6 +4942,7 @@ lighting Light Luce
@msg too many library panels were opened in this session in questa sessione sono stati aperti troppi pannelli libreria
@msg The image is {_1} × {_2} pixels, more than {LARGE_TRACE_SIDE} on a side: it is traced, but more slowly. A lower-fidelity preset or a smaller copy of the image traces faster. L’immagine è di {_1} × {_2} pixel, più di {LARGE_TRACE_SIDE} su un lato: viene vettorializzata, ma più lentamente. Un predefinito a fedeltà più bassa o una copia più piccola dell’immagine è più veloce.
@msg The image was traced at {_1} × {_2} pixels, scaled down from {_3} × {_4} ({_5} megapixels at most). L’immagine è stata vettorializzata a {_1} × {_2} pixel, ridotta da {_3} × {_4} (al massimo {_5} megapixel).
Gap Length: Lunghezza lacuna:
Artboard {n} of {total} Tavola da disegno {n} di {total}
Encoding and writing the file… Codifica e scrittura del file…
@plural {n} more export waiting|{n} more exports waiting {n} altra esportazione in attesa|{n} altre esportazioni in attesa
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4982,6 +4982,7 @@ lighting Light ライト
@msg too many library panels were opened in this session このセッションで開いたライブラリパネルが多すぎます
@msg The image is {_1} × {_2} pixels, more than {LARGE_TRACE_SIDE} on a side: it is traced, but more slowly. A lower-fidelity preset or a smaller copy of the image traces faster. 画像は {_1} × {_2} ピクセルで、一辺が {LARGE_TRACE_SIDE} を超えています。トレースはできますが、時間がかかります。忠実度の低いプリセットや画像の小さいコピーを使うと速くなります。
@msg The image was traced at {_1} × {_2} pixels, scaled down from {_3} × {_4} ({_5} megapixels at most). 画像は {_3} × {_4} から縮小した {_1} × {_2} ピクセルでトレースされました (最大 {_5} メガピクセル)。
Gap Length: ギャップの長さ:
Artboard {n} of {total} アートボード {n} / {total}
Encoding and writing the file… ファイルをエンコードして書き込み中…
@plural {n} more export waiting|{n} more exports waiting 待機中の書き出し: あと {n} 件
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5230,6 +5230,7 @@ imageTrace Outline კონტური
# Select by Color (select.color)
Select by Color მონიშვნა ფერის მიხედვით
@msg thumbnail: no page {n} (the file has 1) thumbnail: გვერდი {n} არ არსებობს (ფაილს 1 გვერდი აქვს)
Gap Length: ღრეჩოს სიგრძე:
Smudge Stick… დასაზელი ჯოხი…
Smudge Stick დასაზელი ჯოხი
Sponge… ღრუბელი…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4905,6 +4905,7 @@ lighting Light 광원
@msg too many library panels were opened in this session 이 세션에서 라이브러리 패널을 너무 많이 열었습니다
@msg The image is {_1} × {_2} pixels, more than {LARGE_TRACE_SIDE} on a side: it is traced, but more slowly. A lower-fidelity preset or a smaller copy of the image traces faster. 이미지가 {_1} × {_2}픽셀로 한 변이 {LARGE_TRACE_SIDE}을 넘습니다. 추적은 되지만 더 느립니다. 충실도가 낮은 사전 설정이나 이미지의 작은 사본을 사용하면 더 빠릅니다.
@msg The image was traced at {_1} × {_2} pixels, scaled down from {_3} × {_4} ({_5} megapixels at most). 이미지를 {_3} × {_4}에서 축소한 {_1} × {_2}픽셀로 추적했습니다(최대 {_5}메가픽셀).
Gap Length: 간격 길이:
Artboard {n} of {total} 아트보드 {n} / {total}
Encoding and writing the file… 파일을 인코딩하고 쓰는 중…
@plural {n} more export waiting|{n} more exports waiting 대기 중인 내보내기 {n}개 더
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5291,6 +5291,7 @@ swatch Bright Violet Felviolet
Typing Typen
Persistent Permanent
Library Panel Options Opties voor bibliotheekpaneel
Gap Length: Lengte van opening:
Locate Library… Bibliotheek lokaliseren…
@msg Save writes {_1}, not {_2}: use file.saveAsTemplate to write it Opslaan schrijft {_1}, niet {_2}: gebruik file.saveAsTemplate om het te schrijven
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5180,6 +5180,7 @@ mark-style Japanese Japońskie
@msg too many library panels were opened in this session w tej sesji otwarto zbyt wiele paneli bibliotek
@msg The image is {_1} × {_2} pixels, more than {LARGE_TRACE_SIDE} on a side: it is traced, but more slowly. A lower-fidelity preset or a smaller copy of the image traces faster. Obraz ma {_1} × {_2} pikseli, ponad {LARGE_TRACE_SIDE} na jednym boku: zostanie obrysowany, ale wolniej. Ustawienie wstępne o niższej wierności lub mniejsza kopia obrazu obrysuje się szybciej.
@msg The image was traced at {_1} × {_2} pixels, scaled down from {_3} × {_4} ({_5} megapixels at most). Obraz obrysowano w rozdzielczości {_1} × {_2} pikseli, po zmniejszeniu z {_3} × {_4} (najwyżej {_5} megapikseli).
Gap Length: Długość przerwy:
Artboard {n} of {total} Obszar kompozycji {n} z {total}
Encoding and writing the file… Kodowanie i zapisywanie pliku…
@plural {n} more export waiting|{n} more exports waiting Czeka jeszcze {n} eksport|Czekają jeszcze {n} eksporty|Czeka jeszcze {n} eksportów
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5245,6 +5245,7 @@ mark-style Japanese Japonesas
@msg too many library panels were opened in this session foram abertos painéis de biblioteca demais nesta sessão
@msg The image is {_1} × {_2} pixels, more than {LARGE_TRACE_SIDE} on a side: it is traced, but more slowly. A lower-fidelity preset or a smaller copy of the image traces faster. A imagem tem {_1} × {_2} pixels, mais de {LARGE_TRACE_SIDE} em um lado: ela é traçada, mas mais devagar. Uma predefinição de menor fidelidade ou uma cópia menor da imagem traça mais rápido.
@msg The image was traced at {_1} × {_2} pixels, scaled down from {_3} × {_4} ({_5} megapixels at most). A imagem foi traçada em {_1} × {_2} pixels, reduzida de {_3} × {_4} (no máximo {_5} megapixels).
Gap Length: Comprimento da Lacuna:
Artboard {n} of {total} Mesa de Trabalho {n} de {total}
Encoding and writing the file… Codificando e gravando o arquivo…
@plural {n} more export waiting|{n} more exports waiting Mais {n} exportação aguardando|Mais {n} exportações aguardando
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -4938,6 +4938,7 @@ lighting Light Свет
@msg too many library panels were opened in this session в этом сеансе открыто слишком много панелей библиотек
@msg The image is {_1} × {_2} pixels, more than {LARGE_TRACE_SIDE} on a side: it is traced, but more slowly. A lower-fidelity preset or a smaller copy of the image traces faster. Изображение {_1} × {_2} пикселей, больше {LARGE_TRACE_SIDE} по одной стороне: трассировка выполняется, но медленнее. Стиль с меньшей точностью или уменьшенная копия изображения трассируются быстрее.
@msg The image was traced at {_1} × {_2} pixels, scaled down from {_3} × {_4} ({_5} megapixels at most). Изображение трассировано в размере {_1} × {_2} пикселей, уменьшенное с {_3} × {_4} (не более {_5} мегапикселей).
Gap Length: Длина зазора:
Artboard {n} of {total} Монтажная область {n} из {total}
Encoding and writing the file… Кодирование и запись файла…
@plural {n} more export waiting|{n} more exports waiting Ещё {n} экспорт в очереди|Ещё {n} экспорта в очереди|Ещё {n} экспортов в очереди
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5284,6 +5284,7 @@ theme Appearance Utseende
@msg too many library panels were opened in this session för många bibliotekspaneler har öppnats i den här sessionen
@msg The image is {_1} × {_2} pixels, more than {LARGE_TRACE_SIDE} on a side: it is traced, but more slowly. A lower-fidelity preset or a smaller copy of the image traces faster. Bilden är {_1} × {_2} pixlar, mer än {LARGE_TRACE_SIDE} på en sida: den vektoriseras, men långsammare. En förinställning med lägre återgivning eller en mindre kopia av bilden går snabbare.
@msg The image was traced at {_1} × {_2} pixels, scaled down from {_3} × {_4} ({_5} megapixels at most). Bilden vektoriserades vid {_1} × {_2} pixlar, nedskalad från {_3} × {_4} (högst {_5} megapixlar).
Gap Length: Lucklängd:
Artboard {n} of {total} Rityta {n} av {total}
Encoding and writing the file… Kodar och skriver filen…
@plural {n} more export waiting|{n} more exports waiting {n} export till väntar|{n} exporter till väntar
Can't render this file because it contains an unexpected character in line 5 and column 25.
+1
View File
@@ -4922,6 +4922,7 @@ lighting Light Світло
@msg too many library panels were opened in this session у цьому сеансі відкрито забагато панелей бібліотек
@msg The image is {_1} × {_2} pixels, more than {LARGE_TRACE_SIDE} on a side: it is traced, but more slowly. A lower-fidelity preset or a smaller copy of the image traces faster. Зображення {_1} × {_2} пікселів, понад {LARGE_TRACE_SIDE} з одного боку: трасування виконується, але повільніше. Стиль із нижчою точністю або менша копія зображення трасуються швидше.
@msg The image was traced at {_1} × {_2} pixels, scaled down from {_3} × {_4} ({_5} megapixels at most). Зображення трасовано в розмірі {_1} × {_2} пікселів, зменшене з {_3} × {_4} (щонайбільше {_5} мегапікселів).
Gap Length: Довжина проміжку:
Artboard {n} of {total} Монтажна область {n} з {total}
Encoding and writing the file… Кодування та запис файлу…
@plural {n} more export waiting|{n} more exports waiting Ще {n} експорт в очікуванні|Ще {n} експорти в очікуванні|Ще {n} експортів в очікуванні
Can't render this file because it contains an unexpected character in line 733 and column 40.
+1
View File
@@ -4922,6 +4922,7 @@ theme Appearance Giao diện
@msg too many library panels were opened in this session đã mở quá nhiều bảng thư viện trong phiên này
@msg The image is {_1} × {_2} pixels, more than {LARGE_TRACE_SIDE} on a side: it is traced, but more slowly. A lower-fidelity preset or a smaller copy of the image traces faster. Ảnh có kích thước {_1} × {_2} điểm ảnh, hơn {LARGE_TRACE_SIDE} ở một cạnh: ảnh vẫn được dò, nhưng chậm hơn. Một thiết lập sẵn có độ trung thực thấp hơn hoặc một bản sao nhỏ hơn của ảnh sẽ dò nhanh hơn.
@msg The image was traced at {_1} × {_2} pixels, scaled down from {_3} × {_4} ({_5} megapixels at most). Ảnh đã được dò ở {_1} × {_2} điểm ảnh, thu nhỏ từ {_3} × {_4} (tối đa {_5} megapixel).
Gap Length: Độ dài khoảng hở:
Artboard {n} of {total} Bảng vẽ {n} / {total}
Encoding and writing the file… Đang mã hóa và ghi tệp…
@plural {n} more export waiting|{n} more exports waiting Còn {n} lượt xuất đang chờ
Can't render this file because it contains an unexpected character in line 4021 and column 10.
+1
View File
@@ -5161,6 +5161,7 @@ lighting Light 光源
@msg too many library panels were opened in this session 本次会话中打开的库面板过多
@msg The image is {_1} × {_2} pixels, more than {LARGE_TRACE_SIDE} on a side: it is traced, but more slowly. A lower-fidelity preset or a smaller copy of the image traces faster. 图像为 {_1} × {_2} 像素,某一边超过 {LARGE_TRACE_SIDE}:会进行描摹,但速度较慢。使用保真度较低的预设或图像的较小副本会更快。
@msg The image was traced at {_1} × {_2} pixels, scaled down from {_3} × {_4} ({_5} megapixels at most). 图像已按 {_1} × {_2} 像素描摹,由 {_3} × {_4} 缩小而来(最多 {_5} 百万像素)。
Gap Length: 间隙长度:
# Links and Attributes: files and URLs handed to other apps.
@msg Edit Original opens image and document files only; “{name}” is not one 编辑原稿只能打开图像和文档文件;“{name}” 不是其中之一
@msg only http, https and mailto URLs open in the browser, not “{url}” 浏览器中只会打开 http、https 和 mailto 的 URL,不会打开 “{url}”
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4956,6 +4956,7 @@ lighting Light 光源
Persistent 持久
Locate Library… 尋找資料庫…
Library Panel Options 資料庫面板選項
Gap Length: 間隙長度:
Artboard {n} of {total} 工作區域 {n} / {total}
Encoding and writing the file… 正在編碼並寫入檔案…
@plural {n} more export waiting|{n} more exports waiting 另有 {n} 個匯出等待中
Can't render this file because it contains an unexpected character in line 6 and column 47.
+11 -4
View File
File diff suppressed because one or more lines are too long
+3 -2
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -1
View File
File diff suppressed because one or more lines are too long