mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 13:47:21 -06:00
Merge pull request #4886 from storytold/fix/4835-duplicate-type
Fix #4835: Artwork Cleanup's Duplicate Objects finds type set twice in the same place
This commit is contained in:
@@ -46,7 +46,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Health Check",
|
||||
["Object", "Plug-ins", "Artwork Cleanup"],
|
||||
None,
|
||||
"{scope?: \"selection\"|\"document\" (default: the selection, or the document while nothing is selected), kinds?: [kind…] (default: all: emptyText, strayPoints (paths of one anchor), unpainted, zeroOpacity, duplicateObjects (a path stacked on an identical one), offArtboard, duplicateAnchors (neighbours within 0.01 pt), tinyHandles (under 0.01 pt), collinearAnchors (corners on a straight run, turning less than sine 0.005), nearPoints (anchors within 0.01 pt of other paths' anchors), nearlyClosed (ends within 0.01 pt), filledOpenPaths (filled, unstroked open paths), needlessClipGroups (an unpainted clip that clips nothing, hides everything, or is a rectangle round all its art), needlessCompounds (one path or none), needlessGroups (one member or none, nothing of their own), emptyArtboards (the document only))} find issues without changing anything; locked and hidden objects are left out → {scope, total, findings: [{kind, label, count, ids (the objects, at most 1000), artboards (indexes)}]} for the kinds found",
|
||||
"{scope?: \"selection\"|\"document\" (default: the selection, or the document while nothing is selected), kinds?: [kind…] (default: all: emptyText, strayPoints (paths of one anchor), unpainted, zeroOpacity, duplicateObjects (a path stacked on an identical one, or type on the same type within 1 pt), offArtboard, duplicateAnchors (neighbours within 0.01 pt), tinyHandles (under 0.01 pt), collinearAnchors (corners on a straight run, turning less than sine 0.005), nearPoints (anchors within 0.01 pt of other paths' anchors), nearlyClosed (ends within 0.01 pt), filledOpenPaths (filled, unstroked open paths), needlessClipGroups (an unpainted clip that clips nothing, hides everything, or is a rectangle round all its art), needlessCompounds (one path or none), needlessGroups (one member or none, nothing of their own), emptyArtboards (the document only))} find issues without changing anything; locked and hidden objects are left out → {scope, total, findings: [{kind, label, count, ids (the objects, at most 1000), artboards (indexes)}]} for the kinds found",
|
||||
has_doc,
|
||||
check
|
||||
),
|
||||
|
||||
@@ -235,17 +235,46 @@ fn is_plain(n: &Node) -> bool {
|
||||
&& *n.extras() == NodeExtras::default()
|
||||
}
|
||||
|
||||
/// Is `n` a plain path (full opacity, normal blending) identical to the object just below it?
|
||||
/// Is `n` a plain path or type object (full opacity, normal blending) that doubles the object just
|
||||
/// below it?
|
||||
fn duplicates_the_one_below(d: &Document, n: &Node) -> bool {
|
||||
if !matches!(n.kind, NodeKind::Path { .. }) || n.opacity < 1.0 || n.blend != BlendMode::Normal {
|
||||
if n.opacity < 1.0 || n.blend != BlendMode::Normal {
|
||||
return false;
|
||||
}
|
||||
let below = d.position(n.id).and_then(|(parent, i, _)| d.children(parent)?.get(i.checked_sub(1)?).cloned());
|
||||
below.is_some_and(|b| {
|
||||
b.visible && b.kind == n.kind && b.appearance == n.appearance && b.opacity == n.opacity && b.blend == n.blend && b.extras() == n.extras()
|
||||
b.visible
|
||||
&& same_art(&b.kind, &n.kind)
|
||||
&& b.appearance == n.appearance
|
||||
&& b.opacity == n.opacity
|
||||
&& b.blend == n.blend
|
||||
&& b.extras() == n.extras()
|
||||
})
|
||||
}
|
||||
|
||||
/// Distance (pt) within which doubled type counts as stacked: text set twice from a PDF or an old
|
||||
/// file, or for a fake bold, is often a fraction of a point off.
|
||||
const DOUBLED_TYPE: f64 = 1.0;
|
||||
|
||||
/// The same path, or the same type (text, styles, size and turn) within [`DOUBLED_TYPE`] of it.
|
||||
fn same_art(a: &NodeKind, b: &NodeKind) -> bool {
|
||||
match (a, b) {
|
||||
(NodeKind::Path { .. }, NodeKind::Path { .. }) => a == b,
|
||||
(NodeKind::Text(a), NodeKind::Text(b)) => {
|
||||
let ([a0, a1, a2, a3, ax, ay], [b0, b1, b2, b3, bx, by]) = (a.xf.as_coeffs(), b.xf.as_coeffs());
|
||||
[a0, a1, a2, a3] == [b0, b1, b2, b3] && (ax - bx).hypot(ay - by) <= DOUBLED_TYPE && {
|
||||
// `b` placed on `a`, with `a`'s layout caches (they are in document space).
|
||||
let mut moved = (**b).clone();
|
||||
moved.xf = a.xf;
|
||||
moved.cached_bounds = a.cached_bounds;
|
||||
moved.cached_baselines.clone_from(&a.cached_baselines);
|
||||
**a == moved
|
||||
}
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn inside(a: Rect, b: Rect) -> bool {
|
||||
a.x0 >= b.x0 && a.y0 >= b.y0 && a.x1 <= b.x1 && a.y1 <= b.y1
|
||||
}
|
||||
|
||||
@@ -226,6 +226,26 @@ fn each_check_finds_and_fixes_its_issue_and_leaves_clean_art_alone() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Type set twice in the same place (from a PDF, or for a fake bold) is a duplicate within a point:
|
||||
/// Show selects the upper copy and Fix removes it. Other text, or the same text further off, stays.
|
||||
#[test]
|
||||
fn doubled_type_within_a_point_is_a_duplicate() {
|
||||
let mut s = session();
|
||||
let text = |s: &mut Session, x: f64, text: &str| id_of(&s.execute("text.create", &json!({"x": x, "y": 100, "text": text})).unwrap());
|
||||
let base = text(&mut s, 20.0, "Total");
|
||||
let doubled = text(&mut s, 20.4, "Total");
|
||||
let other = text(&mut s, 20.0, "Subtotal");
|
||||
let far = text(&mut s, 23.0, "Subtotal");
|
||||
assert_eq!(found(&mut s, "duplicateObjects", "document"), 1);
|
||||
let shown = s.execute("addon.cleanup.show", &json!({"kind": "duplicateObjects", "scope": "document"})).unwrap();
|
||||
assert_eq!(shown["ids"], json!([doubled.0]));
|
||||
s.execute("addon.cleanup.fix", &json!({"kinds": ["duplicateObjects"], "scope": "document"})).unwrap();
|
||||
assert!(node(&s, doubled).is_none());
|
||||
for id in [base, other, far] {
|
||||
assert!(node(&s, id).is_some(), "{id:?} stays");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fix_all_is_one_undo_step() {
|
||||
let mut s = session();
|
||||
|
||||
+1
-1
@@ -5726,7 +5726,7 @@ envelopes, graphs, symbols), are left out.
|
||||
| `strayPoints` | Paths of one anchor (not clipping paths) | Removes them |
|
||||
| `unpainted` | Paths and compound paths with no fill and no stroke | Removes them |
|
||||
| `zeroOpacity` | Objects at 0 % opacity | Removes them |
|
||||
| `duplicateObjects` | A path stacked directly on an identical one (geometry, paint, opacity) | Removes the upper one |
|
||||
| `duplicateObjects` | A path stacked directly on an identical one (geometry, paint, opacity), or type stacked directly on the same type (text, styles, paint) within 1 pt of it | Removes the upper one |
|
||||
| `offArtboard` | Top-level objects that touch no artboard | Removes them |
|
||||
| `duplicateAnchors` | Neighbouring anchors within 0.01 pt | Merges them |
|
||||
| `tinyHandles` | Handles shorter than 0.01 pt | Retracts them |
|
||||
|
||||
Reference in New Issue
Block a user