mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 17:56:50 -06:00
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:
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)];
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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
File diff suppressed because one or more lines are too long
+3
-2
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user