Merge pull request #3061 from nrq6993-a11y/stream/vc-feat1-g2-pathfinder-hard-mix

G2: Effect › Pathfinder › Hard Mix live effect
This commit is contained in:
BFlat
2026-10-21 23:58:57 +07:00
committed by GitHub
26 changed files with 109 additions and 6 deletions
+75 -6
View File
@@ -13,12 +13,13 @@
use std::sync::Arc;
use vectorcraft_doc::color::{Color, Paint};
use vectorcraft_doc::{Appearance, AppearanceItem, Node, NodeId, NodeKind, StrokeLayer};
use vectorcraft_geom::{FillRule, PathData, Rect};
use vectorcraft_pathops::{self as po, PathfinderOp};
/// Effect ids of the Pathfinder submenu with their operation, in Illustrator's menu order.
pub const PATHFINDER_EFFECTS: [(&str, &str, PathfinderOp); 10] = [
pub const PATHFINDER_EFFECTS: [(&str, &str, PathfinderOp); 11] = [
("pathfinder.add", "Add", PathfinderOp::Unite),
("pathfinder.intersect", "Intersect", PathfinderOp::Intersect),
("pathfinder.exclude", "Exclude", PathfinderOp::Exclude),
@@ -29,6 +30,7 @@ pub const PATHFINDER_EFFECTS: [(&str, &str, PathfinderOp); 10] = [
("pathfinder.merge", "Merge", PathfinderOp::Merge),
("pathfinder.crop", "Crop", PathfinderOp::Crop),
("pathfinder.outline", "Outline", PathfinderOp::Outline),
("pathfinder.hardMix", "Hard Mix", PathfinderOp::HardMix),
];
/// Is `id` an Effect → Pathfinder effect?
@@ -138,15 +140,56 @@ pub fn pathfinder_children(group: &Node, hook: Option<OutlineHook>) -> Option<Ve
if shapes.is_empty() {
return Some(vec![]);
}
stack = po::pathfinder(op, &shapes)
.into_iter()
.filter(|r| !r.path.is_empty())
.map(|r| Arc::new(styled(&sources[r.key as usize], r.path, op == PathfinderOp::Outline)))
.collect();
stack = if op == PathfinderOp::HardMix {
po::regions(&shapes)
.into_iter()
.filter(|r| !r.path.is_empty())
.map(|r| Arc::new(hard_mix_face(r.path, &r.sources, &shapes, &sources)))
.collect()
} else {
po::pathfinder(op, &shapes)
.into_iter()
.filter(|r| !r.path.is_empty())
.map(|r| Arc::new(styled(&sources[r.key as usize], r.path, op == PathfinderOp::Outline)))
.collect()
};
}
Some(stack)
}
/// One face of a Hard Mix: the max of each CMYK channel over every covering shape's fill; no stroke.
fn hard_mix_face(path: PathData, covering: &[usize], shapes: &[po::Shape], sources: &[Node]) -> Node {
let mut cmyk = [0.0f32; 4];
let mut painted = false;
for &si in covering {
let src = &sources[shapes[si].key as usize];
let paint = src.appearance.fill_paint();
if let Some(color) = paint.color() {
let tinted = match paint {
Paint::Solid { tint, .. } => color.tinted(tint),
_ => color,
};
let [c, m, y, k] = tinted.to_cmyk();
for (out, v) in cmyk.iter_mut().zip([c, m, y, k]) {
*out = out.max(v);
}
painted = true;
}
}
let fill = if painted {
Paint::solid(Color::cmyk(cmyk[0], cmyk[1], cmyk[2], cmyk[3]))
} else {
Paint::None
};
let top = covering.last().and_then(|&si| sources.get(shapes[si].key as usize));
let mut n = Node::path(top.map_or(NodeId(0), |s| s.id), path, Appearance::basic(fill, Paint::None, 0.0));
if let Some(src) = top {
n.opacity = src.opacity;
n.blend = src.blend;
}
n
}
fn is_container(n: &Node) -> bool {
matches!(n.kind, NodeKind::Group { .. } | NodeKind::Layer { .. })
}
@@ -320,6 +363,32 @@ mod tests {
po::area(n.path_data().unwrap(), FillRule::NonZero).abs()
}
#[test]
fn hard_mix_takes_the_highest_of_each_cmyk_channel_and_drops_strokes() {
let a = Appearance::basic(Paint::solid(Color::cmyk(0.2, 0.66, 0.4, 0.0)), Paint::solid(Color::BLACK), 2.0);
let b = Appearance::basic(Paint::solid(Color::cmyk(0.4, 0.2, 0.3, 0.1)), Paint::solid(Color::BLACK), 2.0);
let mut g = Node::group(
NodeId(10),
vec![
Arc::new(Node::path(NodeId(1), shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), a)),
Arc::new(Node::path(NodeId(2), shapes::rectangle(Rect::new(5.0, 0.0, 15.0, 10.0)), b)),
],
);
g.appearance.effects.push(Effect { id: "pathfinder.hardMix".into(), params: serde_json::json!({}), visible: true });
let faces = pathfinder_children(&g, None).unwrap();
assert_eq!(faces.len(), 3);
let overlap = faces
.iter()
.find(|n| {
let b = n.geometric_bounds().unwrap();
b.x0 >= 4.9 && b.x1 <= 10.1 && b.y0 <= 0.1 && b.y1 >= 9.9
})
.expect("overlap face");
assert!(overlap.appearance.stroke_paint().is_none());
let [c, m, y, k] = overlap.appearance.fill_paint().color().unwrap().to_cmyk();
assert!((c - 0.4).abs() < 0.02 && (m - 0.66).abs() < 0.02 && (y - 0.4).abs() < 0.02 && (k - 0.1).abs() < 0.02, "{c} {m} {y} {k}");
}
#[test]
fn shape_modes_follow_the_pathfinder_panel() {
let add = pathfinder_children(&group_with("pathfinder.add"), None).unwrap();
+1
View File
@@ -381,6 +381,7 @@ fn pf_label(op: PathfinderOp) -> &'static str {
PathfinderOp::Crop => "Crop",
PathfinderOp::Outline => "Outline",
PathfinderOp::MinusBack => "Minus Back",
PathfinderOp::HardMix => "Hard Mix",
}
}
+10
View File
@@ -50,6 +50,9 @@ pub enum PathfinderOp {
Outline,
/// Front-most minus everything behind; keeps the front-most key.
MinusBack,
/// Effect only: split into faces like Divide; overlapping faces take the highest of each CMYK
/// channel from every shape covering them (Illustrator Effect → Pathfinder → Hard Mix).
HardMix,
}
/// One face of the planar arrangement of a set of shapes (Shape Builder / Live Paint face).
@@ -172,6 +175,13 @@ pub fn pathfinder(op: PathfinderOp, shapes: &[Shape]) -> Vec<Shape> {
.collect()
}
PathfinderOp::Outline => outline_edges(&arr, shapes),
PathfinderOp::HardMix => regions_of(&arr)
.into_iter()
.filter_map(|r| {
let key = shapes.get(r.top()?)?.key;
Some(Shape::new(r.path, FillRule::NonZero, key))
})
.collect(),
}
}
+1
View File
@@ -1284,6 +1284,7 @@
Hand-Drawn Ръчно рисувани
Handles Дръжки
Hard Light Твърда светлина
Hard Mix Твърдо смесване
Harmony Rules: Правила за хармония:
Harmony Sets Набори от хармонии
Height Височина
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2906,6 +2906,7 @@ lighting Light Světlo
Darken Ztmavit
Exclusion Vyloučit
Hard Light Tvrdé světlo
Hard Mix Tvrdé míchání
Lighten Zesvětlit
Luminosity Světlost
Overlay Překrýt
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2679,6 +2679,7 @@ lighting Light Lys
Darken Mørkere
Exclusion Udelukkelse
Hard Light Hårdt lys
Hard Mix Hård blanding
Lighten Lysere
Luminosity Luminans
Overlay Overlejring
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -1310,6 +1310,7 @@ file-filter Templates Vorlagen
Hand-Drawn Handgezeichnet
Handles Griffe
Hard Light Hartes Licht
Hard Mix Hart mischen
Harmony Rules: Harmonieregeln:
Harmony Sets Harmoniesätze
Height Höhe
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -1247,6 +1247,7 @@
Hand-Drawn Σχεδιασμένο στο χέρι
Handles Λαβές
Hard Light Έντονο φως
Hard Mix Έντονη ανάμειξη
Harmony Rules: Κανόνες χρωματικής αρμονίας:
Harmony Sets Σύνολα χρωματικής αρμονίας
Height Ύψος
1 # Ελληνικά (el) — VectorCraft
1247
1248
1249
1250
1251
1252
1253
+1
View File
@@ -1316,6 +1316,7 @@
Hand-Drawn Dibujado a mano
Handles Manejadores
Hard Light Luz fuerte
Hard Mix Mezcla definida
Harmony Rules: Reglas de armonía:
Harmony Sets Conjuntos de armonía
Height Altura
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2852,6 +2852,7 @@ lighting Light Valo
Darken Tummenna
Exclusion Poissulkeminen
Hard Light Kova valo
Hard Mix Kova sekoitus
Lighten Vaalenna
Luminosity Luminanssi
Overlay Sulautus
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -1315,6 +1315,7 @@
Hand-Drawn Dessiné à la main
Handles Poignées
Hard Light Lumière crue
Hard Mix Mélange maximal
Harmony Rules: Règles d’harmonie :
Harmony Sets Ensembles d’harmonies
Height Hauteur
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -1684,6 +1684,7 @@ file-filter Templates Sablonok
Handles Fogantyúk
Hard Szigorú
Hard Light Kemény fény
Hard Mix Kemény keverés
Harmony Rules Harmóniaszabályok
Harmony Rules: Harmóniaszabályok:
Harmony Sets Harmóniakészletek
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2679,6 +2679,7 @@ lighting Light Cahaya
Darken Menggelapkan
Exclusion Pengecualian
Hard Light Cahaya keras
Hard Mix Campuran keras
Lighten Menerangkan
Luminosity Luminositas
Overlay Menimpa
1 # Indonesian (id) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation
2679
2680
2681
2682
2683
2684
2685
+1
View File
@@ -1326,6 +1326,7 @@ file-filter Templates Modelli
Hand-Drawn Disegnato a mano
Handles Maniglie
Hard Light Luce intensa
Hard Mix Miscela intensa
Harmony Rules: Regole di armonia:
Harmony Sets Gruppi di armonia
Height Altezza
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -1598,6 +1598,7 @@
Hand Tool 手のひらツール
Handles ハンドル
Hard Light ハードライト
Hard Mix ハードミックス
Harmony Rules: ハーモニールール:
Height 高さ
Height: 高さ:
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2491,6 +2491,7 @@ lighting Light სინათლე
Darken ჩამუქება
Exclusion გამორიცხვა
Hard Light მკვეთრი სინათლე
Hard Mix მკაცრი შერევა
Lighten გაღიავება
Luminosity ნათება
Overlay გადაფარვა
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -1593,6 +1593,7 @@
Hand Tool 손 도구
Handles 핸들
Hard Light 하드 라이트
Hard Mix 하드 믹스
Harmony Rules: 하모니 규칙:
Height 높이
Height: 높이:
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2693,6 +2693,7 @@ lighting Light Licht
Darken Donkerder
Exclusion Uitsluiting
Hard Light Hard licht
Hard Mix Hard mengen
Lighten Lichter
Luminosity Luminositeit
Overlay Bedekken
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2784,6 +2784,7 @@ lighting Light Światło
Darken Ciemniej
Exclusion Wykluczenie
Hard Light Ostre światło
Hard Mix Mieszanie twarde
Lighten Jaśniej
Luminosity Jasność
Overlay Nakładka
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -1137,6 +1137,7 @@
Hand-Drawn Desenhado à Mão
Handles Alças
Hard Light Luz Forte
Hard Mix Mistura definida
Harmony Rules: Regras de Harmonia:
Harmony Sets Conjuntos de Harmonia
Height Altura
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -1324,6 +1324,7 @@ file-filter Templates Шаблоны
Hand-Drawn Нарисованный от руки
Handles Маркеры
Hard Light Жёсткий свет
Hard Mix Жёсткое наложение
Harmony Rules: Правила гармонии:
Harmony Sets Наборы гармонии
Height Высота
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -1286,6 +1286,7 @@
Hand-Drawn Handritad
Handles Handtag
Hard Light Hårt ljus
Hard Mix Hård blandning
Harmony Rules: Harmoniregler:
Harmony Sets Harmoniuppsättningar
Height Höjd
Can't render this file because it contains an unexpected character in line 5 and column 25.
+1
View File
@@ -1320,6 +1320,7 @@ file-filter Templates Шаблони
Hand-Drawn Намальований від руки
Handles Маркери
Hard Light Жорстке світло
Hard Mix Жорстке накладання
Harmony Rules: Правила гармонії:
Harmony Sets Набори гармонії
Height Висота
Can't render this file because it contains an unexpected character in line 726 and column 40.
+1
View File
@@ -1525,6 +1525,7 @@ file-filter Templates Mẫu
Handles Tay nắm
Hard Cứng
Hard Light Ánh sáng mạnh
Hard Mix Trộn cứng
Harmony Rules: Quy tắc hài hòa màu:
Harmony Sets Bộ hài hòa màu
Height Chiều cao
1 # Vietnamese (vi) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation
1525
1526
1527
1528
1529
1530
1531
+1
View File
@@ -1272,6 +1272,7 @@ file-filter Templates 模板
Hand-Drawn 手绘
Handles 手柄
Hard Light 强光
Hard Mix 实色混合
Harmony Rules: 协调规则:
Harmony Sets 色彩调和组
Height 高度
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -1298,6 +1298,7 @@ file-filter Templates 範本
Hand-Drawn 手繪
Handles 控制把手
Hard Light 實光
Hard Mix 實色混合
Harmony Rules: 色彩調和規則:
Harmony Sets 色彩調和組
Height 高度
Can't render this file because it contains an unexpected character in line 6 and column 47.