mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-26 10:47:11 -06:00
View: Proof Setup simulates tritanopia
View › Proof Setup had two color blindness items, protanopia and deuteranopia. A third item, Color blindness – Tritanopia-type, proofs the canvas with a tritanopia simulation, and the Separations Preview panel's Proof Setup list offers it too. `view.proofSetup` takes `target: "tritanopia"`. The simulation follows Brettel, Viénot and Mollon (1997): a color keeps its L and M cone responses, and its S response is replaced so that it lies on one of two half-planes. Both contain the neutral axis; one passes through the 485 nm color and the other through the 660 nm color. The neutral axis runs to the display's white, so grays keep their value. The matrices are computed at compile time from the published inputs cited in the code. The protanopia and deuteranopia matrices are unchanged.
This commit is contained in:
@@ -515,6 +515,7 @@ impl Cms {
|
||||
ProofTarget::LegacyMacRgb => srgb.map(|v| lab::linear_to_srgb(v.powf(1.8))),
|
||||
ProofTarget::Protanopia => simulate_cvd(srgb, &PROTANOPIA),
|
||||
ProofTarget::Deuteranopia => simulate_cvd(srgb, &DEUTERANOPIA),
|
||||
ProofTarget::Tritanopia => TRITANOPIA.simulate(srgb),
|
||||
ProofTarget::WorkingCmyk => {
|
||||
let cmyk = self.srgb_to_cmyk(srgb, proof.intent);
|
||||
self.cmyk_to_srgb(cmyk, proof.simulate_paper)
|
||||
@@ -582,6 +583,106 @@ fn simulate_cvd(srgb: [f32; 3], m: &[[f32; 3]; 3]) -> [f32; 3] {
|
||||
lab::mat_mul(m, lab::srgb_linear(srgb)).map(|v| lab::linear_to_srgb(v.clamp(0.0, 1.0)))
|
||||
}
|
||||
|
||||
// Tritanopia: Brettel, Viénot & Mollon (1997), "Computerized simulation of color appearance for
|
||||
// dichromats", JOSA A 14(10), 2647-2655, https://doi.org/10.1364/JOSAA.14.002647. A color keeps its
|
||||
// L and M cone responses, and its S response is replaced so that it lies on one of two half-planes in
|
||||
// LMS space. Both half-planes contain the neutral axis (black to the display's white, so grays keep
|
||||
// their value); one passes through the 485 nm spectral color and the other through the 660 nm one. A
|
||||
// color uses the half-plane on its own side of the plane through the neutral axis and the S axis.
|
||||
// The matrices are computed at compile time from these inputs:
|
||||
// - linear sRGB → CIE XYZ: IEC 61966-2-1 (D65);
|
||||
// - CIE XYZ → LMS: Smith & Pokorny (1975), as printed in https://daltonlens.org/understanding-cvd-simulation/;
|
||||
// - the 485 nm and 660 nm colors: the Judd-Vos color matching functions,
|
||||
// http://www.cvrl.org/database/data/cmfs/ciexyzjv.csv.
|
||||
const LINEAR_SRGB_TO_XYZ: [[f64; 3]; 3] = [[0.4124, 0.3576, 0.1805], [0.2126, 0.7152, 0.0722], [0.0193, 0.1192, 0.9505]];
|
||||
const XYZ_TO_LMS: [[f64; 3]; 3] = [[0.155_14, 0.543_12, -0.032_86], [-0.155_14, 0.456_84, 0.032_86], [0.0, 0.0, 0.016_08]];
|
||||
const XYZ_485_NM: [f64; 3] = [0.056_985, 0.169_87, 0.586_4];
|
||||
const XYZ_660_NM: [f64; 3] = [0.161_61, 0.061, 0.000_011_906];
|
||||
const TRITANOPIA: Dichromat = Dichromat::tritanope();
|
||||
|
||||
/// A dichromat simulation with two half-planes, as matrices on linear sRGB. `near` applies to colors
|
||||
/// whose dot product with `side` is 0 or more, and `far` applies to the others.
|
||||
struct Dichromat {
|
||||
side: [f32; 3],
|
||||
near: [[f32; 3]; 3],
|
||||
far: [[f32; 3]; 3],
|
||||
}
|
||||
|
||||
impl Dichromat {
|
||||
const fn tritanope() -> Self {
|
||||
let to_lms = mat3_mul(&XYZ_TO_LMS, &LINEAR_SRGB_TO_XYZ);
|
||||
let to_rgb = mat3_inverse(&to_lms);
|
||||
let white = mat3_apply(&to_lms, [1.0; 3]);
|
||||
let a485 = mat3_apply(&XYZ_TO_LMS, XYZ_485_NM);
|
||||
let a660 = mat3_apply(&XYZ_TO_LMS, XYZ_660_NM);
|
||||
let mut side = cross(white, [0.0, 0.0, 1.0]);
|
||||
if dot(side, a485) < 0.0 {
|
||||
side = [-side[0], -side[1], -side[2]];
|
||||
}
|
||||
// A color on the 485 nm side lands on that half-plane.
|
||||
let near = mat3_mul(&to_rgb, &mat3_mul(&keep_lm_on_plane(cross(white, a485)), &to_lms));
|
||||
let far = mat3_mul(&to_rgb, &mat3_mul(&keep_lm_on_plane(cross(white, a660)), &to_lms));
|
||||
// `side · lms(rgb)` is `(to_lmsᵀ · side) · rgb`.
|
||||
let t = &to_lms;
|
||||
let side_rgb = [
|
||||
t[0][0] * side[0] + t[1][0] * side[1] + t[2][0] * side[2],
|
||||
t[0][1] * side[0] + t[1][1] * side[1] + t[2][1] * side[2],
|
||||
t[0][2] * side[0] + t[1][2] * side[1] + t[2][2] * side[2],
|
||||
];
|
||||
Dichromat { side: vec3_f32(side_rgb), near: mat3_f32(&near), far: mat3_f32(&far) }
|
||||
}
|
||||
|
||||
fn simulate(&self, srgb: [f32; 3]) -> [f32; 3] {
|
||||
let rgb = lab::srgb_linear(srgb);
|
||||
let s = self.side;
|
||||
let m = if s[0] * rgb[0] + s[1] * rgb[1] + s[2] * rgb[2] >= 0.0 { &self.near } else { &self.far };
|
||||
lab::mat_mul(m, rgb).map(|v| lab::linear_to_srgb(v.clamp(0.0, 1.0)))
|
||||
}
|
||||
}
|
||||
|
||||
/// LMS → LMS: keeps L and M and sets S so the result lies in the plane with normal `n`.
|
||||
const fn keep_lm_on_plane(n: [f64; 3]) -> [[f64; 3]; 3] {
|
||||
[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [-n[0] / n[2], -n[1] / n[2], 0.0]]
|
||||
}
|
||||
|
||||
const fn cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
|
||||
[a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]
|
||||
}
|
||||
|
||||
const fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
|
||||
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
|
||||
}
|
||||
|
||||
const fn mat3_apply(m: &[[f64; 3]; 3], v: [f64; 3]) -> [f64; 3] {
|
||||
[dot(m[0], v), dot(m[1], v), dot(m[2], v)]
|
||||
}
|
||||
|
||||
const fn mat3_mul(a: &[[f64; 3]; 3], b: &[[f64; 3]; 3]) -> [[f64; 3]; 3] {
|
||||
let mut out = [[0.0; 3]; 3];
|
||||
let mut i = 0;
|
||||
while i < 3 {
|
||||
out[i] = [dot(a[i], [b[0][0], b[1][0], b[2][0]]), dot(a[i], [b[0][1], b[1][1], b[2][1]]), dot(a[i], [b[0][2], b[1][2], b[2][2]])];
|
||||
i += 1;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
const fn mat3_inverse(m: &[[f64; 3]; 3]) -> [[f64; 3]; 3] {
|
||||
let c0 = cross(m[1], m[2]);
|
||||
let c1 = cross(m[2], m[0]);
|
||||
let c2 = cross(m[0], m[1]);
|
||||
let det = dot(m[0], c0);
|
||||
[[c0[0] / det, c1[0] / det, c2[0] / det], [c0[1] / det, c1[1] / det, c2[1] / det], [c0[2] / det, c1[2] / det, c2[2] / det]]
|
||||
}
|
||||
|
||||
const fn vec3_f32(v: [f64; 3]) -> [f32; 3] {
|
||||
[v[0] as f32, v[1] as f32, v[2] as f32]
|
||||
}
|
||||
|
||||
const fn mat3_f32(m: &[[f64; 3]; 3]) -> [[f32; 3]; 3] {
|
||||
[vec3_f32(m[0]), vec3_f32(m[1]), vec3_f32(m[2])]
|
||||
}
|
||||
|
||||
/// View → Proof Setup target.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", content = "profile", rename_all = "camelCase")]
|
||||
@@ -601,6 +702,8 @@ pub enum ProofTarget {
|
||||
Protanopia,
|
||||
/// Colour blindness — deuteranopia type.
|
||||
Deuteranopia,
|
||||
/// Color blindness, tritanopia type.
|
||||
Tritanopia,
|
||||
}
|
||||
|
||||
impl ProofTarget {
|
||||
@@ -615,6 +718,7 @@ impl ProofTarget {
|
||||
"monitorRgb" | "monitor" => ProofTarget::MonitorRgb,
|
||||
"protanopia" => ProofTarget::Protanopia,
|
||||
"deuteranopia" => ProofTarget::Deuteranopia,
|
||||
"tritanopia" => ProofTarget::Tritanopia,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
@@ -627,6 +731,7 @@ impl ProofTarget {
|
||||
ProofTarget::MonitorRgb => "monitorRgb".into(),
|
||||
ProofTarget::Protanopia => "protanopia".into(),
|
||||
ProofTarget::Deuteranopia => "deuteranopia".into(),
|
||||
ProofTarget::Tritanopia => "tritanopia".into(),
|
||||
}
|
||||
}
|
||||
/// Whether the target simulates a CMYK press (separations make sense).
|
||||
|
||||
@@ -194,6 +194,35 @@ fn colour_blindness_proofs() {
|
||||
assert_eq!(ProofTarget::parse(&ProofTarget::Cmyk("X".into()).id()), Some(ProofTarget::Cmyk("X".into())));
|
||||
}
|
||||
|
||||
/// The expected colors were computed separately with NumPy from the same published inputs (linear
|
||||
/// sRGB to XYZ from IEC 61966-2-1, Smith & Pokorny's XYZ to LMS, Judd-Vos XYZ at 485 nm and 660 nm),
|
||||
/// following Brettel, Viénot & Mollon (1997) with the display's white as the neutral axis.
|
||||
#[test]
|
||||
fn tritanopia_proof_follows_brettel_1997() {
|
||||
let c = cms();
|
||||
let t = ProofSetup { target: ProofTarget::Tritanopia, ..Default::default() };
|
||||
assert_eq!(ProofTarget::parse("tritanopia"), Some(ProofTarget::Tritanopia));
|
||||
assert_eq!(ProofTarget::Tritanopia.id(), "tritanopia");
|
||||
for g in [0.0, 0.25, 0.5, 0.75, 1.0] {
|
||||
let out = c.proof_srgb([g; 3], &t);
|
||||
assert!(out.iter().all(|v| (v - g).abs() < 1e-4), "gray {g} stays gray: {out:?}");
|
||||
}
|
||||
let cases = [
|
||||
// The 660 nm half-plane.
|
||||
([1.0, 0.0, 0.0], [1.0, 0.0, 0.299_231]),
|
||||
([1.0, 0.0, 1.0], [0.934_206, 0.388_351, 0.467_178]),
|
||||
([0.9, 0.6, 0.1], [0.926_825, 0.557_667, 0.599_649]),
|
||||
// The 485 nm half-plane.
|
||||
([0.0, 0.0, 1.0], [0.0, 0.377_268, 0.519_933]),
|
||||
([0.0, 1.0, 0.0], [0.480_503, 0.920_568, 1.0]),
|
||||
([0.2, 0.4, 0.8], [0.0, 0.464_73, 0.563_516]),
|
||||
];
|
||||
for (rgb, want) in cases {
|
||||
let got = c.proof_srgb(rgb, &t);
|
||||
assert!(got.iter().zip(want).all(|(g, w)| (g - w).abs() < 1e-3), "{rgb:?}: {got:?}, expected {want:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_icc_rejected() {
|
||||
assert!(matches!(register_icc(b"not a profile", None), Err(CmsError::BadProfile(_))));
|
||||
|
||||
@@ -84,7 +84,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Proof Setup",
|
||||
["View", "Proof Setup"],
|
||||
None,
|
||||
"{target?: \"workingCmyk\"|\"cmyk:<profile>\"|\"legacyMacRgb\"|\"srgb\"|\"monitorRgb\"|\"protanopia\"|\"deuteranopia\", intent?, simulatePaper?, proof?: bool (also turn Proof Colors on)} → proof state",
|
||||
"{target?: \"workingCmyk\"|\"cmyk:<profile>\"|\"legacyMacRgb\"|\"srgb\"|\"monitorRgb\"|\"protanopia\"|\"deuteranopia\"|\"tritanopia\", intent?, simulatePaper?, proof?: bool (also turn Proof Colors on)} → proof state",
|
||||
always,
|
||||
proof_setup
|
||||
),
|
||||
|
||||
@@ -311,6 +311,22 @@ fn view_toggles_redraw_without_dirtying() {
|
||||
s.execute("view.overprintPreview", &json!({"on": false})).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proof_setup_simulates_tritanopia() {
|
||||
let _g = GLOBAL.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let mut s = session();
|
||||
rect(&mut s, 0.0, 0.0, 100.0, 100.0, json!("#0000ff"));
|
||||
let r = s.execute("view.proofSetup", &json!({"target": "tritanopia", "proof": true})).unwrap();
|
||||
assert_eq!(r["target"], "tritanopia");
|
||||
let setup = vectorcraft_render::proof::active_proof().unwrap();
|
||||
assert_eq!(setup.target, ProofTarget::Tritanopia);
|
||||
let doc = s.doc().unwrap().doc.clone();
|
||||
let px = render(&doc, RenderOptions { proof: Some(setup), ..Default::default() }).pixel(50, 50);
|
||||
// A tritanope sees blue as a dark teal.
|
||||
assert!(px[0] < 8 && px[1] > 80 && px[2] > 110 && px[2] < 150, "{px:?}");
|
||||
s.execute("view.proofSetup", &json!({"target": "workingCmyk", "proof": false})).unwrap();
|
||||
}
|
||||
|
||||
/// Whether the content has a DeviceCMYK fill/stroke operator (`c m y k k` / `K`).
|
||||
fn has_cmyk_op(pdf: &str) -> bool {
|
||||
let toks: Vec<&str> = pdf.split_ascii_whitespace().collect();
|
||||
|
||||
@@ -479,6 +479,7 @@
|
||||
Monitor RGB RGB monitoru
|
||||
Color blindness – Protanopia-type Barvoslepost – protanopie
|
||||
Color blindness – Deuteranopia-type Barvoslepost – deuteranopie
|
||||
Color blindness – Tritanopia-type Barvoslepost – tritanopie
|
||||
Customize… Vlastní…
|
||||
Proof Colors Barvy nátisku
|
||||
Zoom In Zvětšit
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -469,6 +469,7 @@
|
||||
Color bars Farbkontrollstreifen
|
||||
Color blindness – Deuteranopia-type Farbenblindheit – Deuteranopie
|
||||
Color blindness – Protanopia-type Farbenblindheit – Protanopie
|
||||
Color blindness – Tritanopia-type Farbenblindheit – Tritanopie
|
||||
Color Burn Farbig nachbelichten
|
||||
Color conversion: Farbkonvertierung:
|
||||
Color Dodge Farbig abwedeln
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -484,6 +484,7 @@
|
||||
Color bars Barras de color
|
||||
Color blindness – Deuteranopia-type Daltonismo – tipo deuteranopía
|
||||
Color blindness – Protanopia-type Daltonismo – tipo protanopía
|
||||
Color blindness – Tritanopia-type Daltonismo – tipo tritanopía
|
||||
Color Burn Subexponer color
|
||||
Color conversion: Conversión de color:
|
||||
Color Dodge Sobreexponer color
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -485,6 +485,7 @@
|
||||
Color bars Barres de couleurs
|
||||
Color blindness – Deuteranopia-type Daltonisme – type deutéranopie
|
||||
Color blindness – Protanopia-type Daltonisme – type protanopie
|
||||
Color blindness – Tritanopia-type Daltonisme – type tritanopie
|
||||
Color Burn Densité couleur +
|
||||
Color conversion: Conversion des couleurs :
|
||||
Color Dodge Densité couleur -
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -657,6 +657,7 @@
|
||||
Color bars Színsávok
|
||||
Color blindness – Deuteranopia-type Színvakság – deuteranópia
|
||||
Color blindness – Protanopia-type Színvakság – protanópia
|
||||
Color blindness – Tritanopia-type Színvakság – tritanópia
|
||||
Color Burn Színsötétítés
|
||||
Color conversion: Színátalakítás:
|
||||
Color Dodge Színvilágosítás
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -484,6 +484,7 @@
|
||||
Color bars Barre colore
|
||||
Color blindness – Deuteranopia-type Daltonismo – tipo deuteranopia
|
||||
Color blindness – Protanopia-type Daltonismo – tipo protanopia
|
||||
Color blindness – Tritanopia-type Daltonismo – tipo tritanopia
|
||||
Color Burn Brucia colore
|
||||
Color conversion: Conversione colore:
|
||||
Color Dodge Scherma colore
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -474,6 +474,7 @@
|
||||
Monitor RGB モニター RGB
|
||||
Color blindness – Protanopia-type P 型(1 型)色覚
|
||||
Color blindness – Deuteranopia-type D 型(2 型)色覚
|
||||
Color blindness – Tritanopia-type T 型(3 型)色覚
|
||||
Customize… カスタマイズ…
|
||||
Proof Colors 色の校正
|
||||
Zoom In ズームイン
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -470,6 +470,7 @@
|
||||
Monitor RGB 모니터 RGB
|
||||
Color blindness – Protanopia-type 색맹 – 적색맹 유형
|
||||
Color blindness – Deuteranopia-type 색맹 – 녹색맹 유형
|
||||
Color blindness – Tritanopia-type 색맹 – 청색맹 유형
|
||||
Customize… 사용자 지정…
|
||||
Proof Colors 교정 색상
|
||||
Zoom In 확대
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -436,6 +436,7 @@
|
||||
Monitor RGB RGB monitora
|
||||
Color blindness – Protanopia-type Daltonizm – typu protanopia
|
||||
Color blindness – Deuteranopia-type Daltonizm – typu deuteranopia
|
||||
Color blindness – Tritanopia-type Daltonizm – typu tritanopia
|
||||
Customize… Dostosuj…
|
||||
Proof Colors Próba kolorów
|
||||
Zoom In Powiększ
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -475,6 +475,7 @@
|
||||
Color Type: Tipo de Cor:
|
||||
Color blindness – Deuteranopia-type Daltonismo – tipo deuteranopia
|
||||
Color blindness – Protanopia-type Daltonismo – tipo protanopia
|
||||
Color blindness – Tritanopia-type Daltonismo – tipo tritanopia
|
||||
Color conversion: Conversão de cor:
|
||||
Color images Imagens coloridas
|
||||
Color: Cor:
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -482,6 +482,7 @@
|
||||
Color bars Цветные полосы
|
||||
Color blindness – Deuteranopia-type Дальтонизм — дейтеранопия
|
||||
Color blindness – Protanopia-type Дальтонизм — протанопия
|
||||
Color blindness – Tritanopia-type Дальтонизм — тританопия
|
||||
Color Burn Затемнение основы
|
||||
Color conversion: Преобразование цвета:
|
||||
Color Dodge Осветление основы
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -478,6 +478,7 @@
|
||||
Color bars Кольорові смуги
|
||||
Color blindness – Deuteranopia-type Дальтонізм - дейтеранопія
|
||||
Color blindness – Protanopia-type Дальтонізм - протанопія
|
||||
Color blindness – Tritanopia-type Дальтонізм - тританопія
|
||||
Color Burn Затемнення основи
|
||||
Color conversion: Перетворення кольору:
|
||||
Color Dodge Освітлення основи
|
||||
|
||||
|
@@ -570,6 +570,7 @@
|
||||
Color bars Thanh màu
|
||||
Color blindness – Deuteranopia-type Mù màu – loại Deuteranopia
|
||||
Color blindness – Protanopia-type Mù màu – loại Protanopia
|
||||
Color blindness – Tritanopia-type Mù màu – loại Tritanopia
|
||||
Color Burn Đốt màu
|
||||
Color conversion: Chuyển đổi màu:
|
||||
Color Dodge Tránh màu
|
||||
|
||||
|
@@ -443,6 +443,7 @@
|
||||
Color bars 色彩条
|
||||
Color blindness – Deuteranopia-type 色盲 – 绿色盲型
|
||||
Color blindness – Protanopia-type 色盲 – 红色盲型
|
||||
Color blindness – Tritanopia-type 色盲 – 蓝色盲型
|
||||
Color Burn 颜色加深
|
||||
Color conversion: 色彩转换:
|
||||
Color Dodge 颜色减淡
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -457,6 +457,7 @@
|
||||
Color bars 色彩條
|
||||
Color blindness – Deuteranopia-type 色盲 – 綠色盲型
|
||||
Color blindness – Protanopia-type 色盲 – 紅色盲型
|
||||
Color blindness – Tritanopia-type 色盲 – 藍色盲型
|
||||
Color Burn 加深顏色
|
||||
Color conversion: 色彩轉換:
|
||||
Color Dodge 加亮顏色
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -2591,6 +2591,7 @@ pub fn menu_tree_named(english_names: bool) -> Vec<(&'static str, Vec<Item>)> {
|
||||
cp("Monitor RGB", "view.proofSetup", json!({"target": "monitorRgb", "proof": true})),
|
||||
cp("Color blindness – Protanopia-type", "view.proofSetup", json!({"target": "protanopia", "proof": true})),
|
||||
cp("Color blindness – Deuteranopia-type", "view.proofSetup", json!({"target": "deuteranopia", "proof": true})),
|
||||
cp("Color blindness – Tritanopia-type", "view.proofSetup", json!({"target": "tritanopia", "proof": true})),
|
||||
cp("Customize…", "window.panel", json!({"panel": "separations"})),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -12,13 +12,14 @@ use crate::VectorcraftApp;
|
||||
use crate::theme::Tokens;
|
||||
use crate::widgets::{self, dim_label, menu_item};
|
||||
|
||||
const PROOF_TARGETS: [(&str, &str); 6] = [
|
||||
const PROOF_TARGETS: [(&str, &str); 7] = [
|
||||
("workingCmyk", "Working CMYK"),
|
||||
("legacyMacRgb", "Legacy Macintosh RGB (Gamma 1.8)"),
|
||||
("srgb", "Internet Standard RGB (sRGB)"),
|
||||
("monitorRgb", "Monitor RGB"),
|
||||
("protanopia", "Color blindness – Protanopia-type"),
|
||||
("deuteranopia", "Color blindness – Deuteranopia-type"),
|
||||
("tritanopia", "Color blindness – Tritanopia-type"),
|
||||
];
|
||||
|
||||
fn run(app: &mut VectorcraftApp, id: &str, p: Value) {
|
||||
@@ -185,3 +186,25 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
run(app, "view.separationsPreview", json!({"on": true}));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use vectorcraft_engine::Session;
|
||||
|
||||
/// The panel's Proof Setup list offers the targets of View › Proof Setup, in the same order and
|
||||
/// with the same labels, and each one is a target `view.proofSetup` accepts.
|
||||
#[test]
|
||||
fn proof_targets_match_the_proof_setup_menu() {
|
||||
let app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
let menu: Vec<(String, String)> = crate::menus::menu_entries(&app)
|
||||
.into_iter()
|
||||
.filter(|e| e.path == ["View", "Proof Setup"] && e.command.as_deref() == Some("view.proofSetup"))
|
||||
.map(|e| (e.params["target"].as_str().unwrap_or_default().to_string(), e.label))
|
||||
.collect();
|
||||
let panel: Vec<(String, String)> = PROOF_TARGETS.iter().map(|(id, label)| (id.to_string(), label.to_string())).collect();
|
||||
assert_eq!(menu, panel);
|
||||
assert!(panel.iter().any(|(id, _)| id == "tritanopia"));
|
||||
assert!(panel.iter().all(|(id, _)| cms::ProofTarget::parse(id).is_some_and(|t| t.id() == *id)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -507,6 +507,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- **Paint and appearance (M3):**
|
||||
- **Swatches:** names follow the colour model and are unique; Swatch Options (process/spot, Global, Gray/RGB/HSB/CMYK/Web, live preview); editing a global or spot swatch recolours every linked fill, stroke and text run in one undo step; multi-select and delete with confirmation; New Swatch and New Color Group dialogs (a group from the selected artwork's colours); swatches in colour groups convert, separate and count like any other. The Swatches panel selects ranges and whole groups, has a find field, and drags swatches to reorder, regroup or paint art; Add Used Colors, Select All Unused, Merge, Ungroup and Sort by Kind; a CMYK document keeps applied colours in CMYK. Swatch libraries: nine generated colour libraries and five gradient libraries in a library panel, saved as `.vcswatches`, `.gpl`, swatch exchange (`.ase`) or CSS, and loaded from `.vcswatches`, `.gpl` and `.ase` files (`.ase` both ways: RGB, CMYK, Lab and Gray; global, spot and process; groups) and from color books (`.acb`, read only: the RGB, CMYK or Lab colors of a book such as a Pantone book a user owns, named with its prefix and suffix, spot unless it says process, #831); `swatch.library.copyToUser` copies a loaded library into the User Defined folder without replacing a file, and a file with the same extension and bytes as a User Defined library opens as that library. Tints of global and spot colours ("Name 40%") stay linked and separate as a percentage of their plate. Spot colours can be defined in Lab (Spot Colors options). A built-in [Registration] swatch prints on every plate.
|
||||
- **Colour:** a Color Picker (colour field, H/S/B/R/G/B channel slider, HSB/RGB/Lab/CMYK fields, hex, web-only snapping, out-of-gamut correction); the Color panel shows a selected colour in its own model and, with nothing selected, the new-art defaults in the mode picked last, which it keeps between sessions; Alt-click paints the other proxy; proxies look through groups and show "?" for mixed paint; Invert, Complement, Apply Last Color (`,`) and Apply Last Gradient (`.`) keep the colour model; every paint command feeds the recent colours. The Color Guide has 21 harmony rules, Steps and Variation options and Limit to Library. Adjust Color Balance (its Global mode shifts tints) and Saturate preview live; Edit Colors and Recolor also reach meshes, images and patterns. Recolor Artwork has colour reduction, five recolour methods, presets, Limit to Library, a colour-wheel Edit tab and Edit Color Group; a local Color Themes panel saves five-colour themes. Fill/Stroke chips open swatch and mixer popovers (the Properties panel has the Control bar's Fill and Stroke chips, Stroke link and weight spinner, with nothing selected too), panels have Illustrator's Window menu keys (Align Shift+F7, Transform Shift+F8, Pathfinder Cmd+Shift+F9, Layers F7, Character Cmd+T…; every panel can be given one in Edit › Keyboard Shortcuts), and Eyedropper Options pick up and apply chosen attributes (Alt and Shift+Alt, image pixels). An ICC CMYK profile converts straight into the RGB working space (not through sRGB, so a wider space such as Wide Gamut RGB keeps colours sRGB can't hold) with the colour settings' black-point compensation, on the canvas and in RGB exports alike. Fills and strokes can overprint (Attributes panel, Overprint Black options), and CMYK documents blend in inks on screen, in PDF and when flattening. Document Color Mode converts the art, symbols, pattern tiles and swatches through the colour settings: Gray stays Gray, RGB greys separate into four-colour greys and a rich black as Illustrator does, or go on the black plate only with `grays: "black"` (also for `document.open {colorMode}`).
|
||||
View › Proof Setup › Color blindness – Tritanopia-type turns Proof Colors on and proofs the canvas with the tritanopia simulation of Brettel, Viénot and Mollon (1997). The Separations Preview panel's Proof Setup list offers the same target (`view.proofSetup {target: "tritanopia"}`). Grays keep their value.
|
||||
- **Eyedropper sources (#1251):** ordinary and Shift clicks, Shift+Alt appearance append, raster pixels and gradient-stop sampling can read visible locked/template art and sources outside isolation. Sampling leaves the source and selection intact; Alt reverse still uses editable hits, and hidden art stays excluded. Engine/tool/hit regressions cover the editing boundary, color models and one-step Undo/Redo. The general object/image workflow is described in [public HelpX](https://helpx.adobe.com/illustrator/using/tool-techniques/eyedropper-tool.html); the lock/isolation expectations come from the user report and these regressions, without a new full-parity measurement.
|
||||
- **Sample Color from Screen:** an eyedropper button in the Color Picker and `ui.sampleScreenColor` (which can be given a key in Keyboard Shortcuts) pick a color anywhere on the screen with the system's sampler: `NSColorSampler` on macOS, the EyeDropper API in browsers that have it. The color becomes the fill or the stroke as one undo step, or the Color Picker's color, converted from display sRGB into the working RGB space. Windows, Linux and other browsers have no sampler yet; the button is gray there.
|
||||
- **Stroke visibility (#1253):** adding paint to a previously unpainted zero-width stroke restores the existing 1 pt default (`PaintDefaults::default`, `Appearance::set_stroke`), for the selected appearance item, the new-art defaults and Swap Fill/Stroke. Positive widths are kept; recoloring a painted zero-width stroke keeps its explicit zero. A real Eyedropper-to-new-shape sequence reproduces the zero-width inheritance. Five engine regressions cover creation, defaults, targeted items, preserved widths and one-step Undo/Redo (`crates/engine/tests/stroke_visibility.rs`).
|
||||
|
||||
Reference in New Issue
Block a user