mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-28 19:57:05 -06:00
Merge wp/F2: knockout groups, knockout shape, page groups (M3.46-M3.48)
# Conflicts: # crates/pdf/src/export.rs # crates/svg/src/export.rs
This commit is contained in:
@@ -55,11 +55,7 @@ pub(crate) fn mask_group(children: Vec<Arc<Node>>) -> Node {
|
||||
fn is_mask_group(n: &Node) -> bool {
|
||||
matches!(n.kind, NodeKind::Group { clip: false, .. })
|
||||
&& n.name.as_deref() == Some(MASK_GROUP)
|
||||
&& n.opacity >= 1.0
|
||||
&& n.blend == vectorcraft_doc::color::BlendMode::Normal
|
||||
&& !n.isolate
|
||||
&& !n.knockout
|
||||
&& n.mask.is_none()
|
||||
&& n.has_default_transparency()
|
||||
&& n.appearance.items.is_empty()
|
||||
&& n.appearance.effects.is_empty()
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::sync::Arc;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_color::{BlendMode, Paint};
|
||||
use vectorcraft_doc::{Appearance, Document, Node, NodeId, NodeKind};
|
||||
use vectorcraft_doc::{Appearance, Document, Knockout, Node, NodeId, NodeKind};
|
||||
use vectorcraft_geom::{Affine, FillRule, Point, Rect, Vec2};
|
||||
|
||||
use super::edit::{duplicate_in, selected_roots};
|
||||
@@ -131,7 +131,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Object Properties",
|
||||
[],
|
||||
None,
|
||||
"{ids?|id?, name?, visible?, locked?, opacity?: 0..100, blend?: \"Multiply\"…, isolate?, knockout?}",
|
||||
"{ids?|id?, name?, visible?, locked?, opacity?: 0..100, blend?: \"Multiply\"…, isolate?, knockout?: \"on\"|\"off\"|\"neutral\"|bool (true = on, false = neutral), knockoutShape?: bool}",
|
||||
has_doc,
|
||||
set_props
|
||||
),
|
||||
@@ -644,6 +644,13 @@ fn set_props(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Some(b) => Some(BlendMode::parse(b).ok_or_else(|| bad("object.setProps", format!("unknown blend mode `{b}`")))?),
|
||||
None => None,
|
||||
};
|
||||
let knockout = match p.get("knockout") {
|
||||
Some(v) => Some(
|
||||
Knockout::from_value(v)
|
||||
.ok_or_else(|| bad("object.setProps", format!("knockout must be \"on\", \"off\", \"neutral\" or a bool, not {v}")))?,
|
||||
),
|
||||
None => None,
|
||||
};
|
||||
s.edit("Object Properties", |d, _| {
|
||||
for id in &ids {
|
||||
let n = d.node_mut(*id).ok_or(EngineError::NoNode(*id))?;
|
||||
@@ -665,8 +672,11 @@ fn set_props(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if let Some(v) = p.get("isolate").and_then(Value::as_bool) {
|
||||
n.isolate = v;
|
||||
}
|
||||
if let Some(v) = p.get("knockout").and_then(Value::as_bool) {
|
||||
n.knockout = v;
|
||||
if let Some(k) = knockout {
|
||||
n.knockout = k;
|
||||
}
|
||||
if let Some(v) = p.get("knockoutShape").and_then(Value::as_bool) {
|
||||
n.knockout_shape = v;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -10,7 +10,7 @@ use std::collections::HashSet;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_color::BlendMode;
|
||||
use vectorcraft_doc::{Document, Node, NodeId, OpacityMask, Selection};
|
||||
use vectorcraft_doc::{Document, Knockout, Node, NodeId, OpacityMask, Selection};
|
||||
|
||||
use super::edit::selected_roots;
|
||||
use super::maskedit::{begin, finish, mask_group, mask_parts, show_editing};
|
||||
@@ -108,10 +108,28 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Transparency Info",
|
||||
[],
|
||||
None,
|
||||
"{id?|ids?} → {ids, opacity: 0..100, blend, isolate, knockout, editingMask} the Transparency panel's values for `ids`, the selection, or the object whose mask is being edited. A value is null where those objects differ (or there are none); editingMask is the id of the object whose mask is being edited, or null",
|
||||
"{id?|ids?} → {ids, opacity: 0..100, blend, isolate, knockout: \"on\"|\"off\"|\"neutral\", knockoutShape, editingMask, pageIsolatedBlending, pageKnockoutGroup} the Transparency panel's values for `ids`, the selection, or the object whose mask is being edited. A value is null where those objects differ (or there are none); editingMask is the id of the object whose mask is being edited, or null; the page values are the document's",
|
||||
has_doc,
|
||||
info
|
||||
),
|
||||
cmd!(
|
||||
"transparency.togglePageIsolatedBlending",
|
||||
"Page Isolated Blending",
|
||||
["Window", "Transparency"],
|
||||
None,
|
||||
"{value?} make the page an isolated group (default: toggle): blend modes of top-level objects don't blend with what lies under the page; saved with the document and written to PDF → {value}",
|
||||
has_doc,
|
||||
|s, p| toggle_page(s, p, false)
|
||||
),
|
||||
cmd!(
|
||||
"transparency.togglePageKnockoutGroup",
|
||||
"Page Knockout Group",
|
||||
["Window", "Transparency"],
|
||||
None,
|
||||
"{value?} make the page a knockout group (default: toggle): its layers (the contents of neutral layers) hide what they cover instead of showing it through their transparency; saved with the document and written to PDF → {value}",
|
||||
has_doc,
|
||||
|s, p| toggle_page(s, p, true)
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -248,6 +266,24 @@ fn toggle_new_clip(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(json!({ "value": v }))
|
||||
}
|
||||
|
||||
/// Set (or toggle) the page's knockout group flag (`knockout`) or isolated blending flag, as one
|
||||
/// undo step; setting the current value changes nothing.
|
||||
fn toggle_page(s: &mut Session, p: &Value, knockout: bool) -> Result<Value> {
|
||||
fn flag(d: &mut Document, knockout: bool) -> &mut bool {
|
||||
if knockout { &mut d.page_knockout } else { &mut d.page_isolate }
|
||||
}
|
||||
let d = &s.doc()?.doc;
|
||||
let cur = if knockout { d.page_knockout } else { d.page_isolate };
|
||||
let v = bool_or(p, "value", !cur);
|
||||
if v != cur {
|
||||
s.edit(if knockout { "Page Knockout Group" } else { "Page Isolated Blending" }, |d, _| {
|
||||
*flag(d, knockout) = v;
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
Ok(json!({ "value": v }))
|
||||
}
|
||||
|
||||
fn toggle_new_invert(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let v = bool_or(p, "value", !s.menu.new_masks_inverted);
|
||||
s.menu.new_masks_inverted = v;
|
||||
@@ -264,7 +300,8 @@ pub struct TransparencyInfo {
|
||||
pub opacity: Option<f64>,
|
||||
pub blend: Option<BlendMode>,
|
||||
pub isolate: Option<bool>,
|
||||
pub knockout: Option<bool>,
|
||||
pub knockout: Option<Knockout>,
|
||||
pub knockout_shape: Option<bool>,
|
||||
}
|
||||
|
||||
impl TransparencyInfo {
|
||||
@@ -280,6 +317,7 @@ impl TransparencyInfo {
|
||||
blend: same(&nodes, |n| n.blend),
|
||||
isolate: same(&nodes, |n| n.isolate),
|
||||
knockout: same(&nodes, |n| n.knockout),
|
||||
knockout_shape: same(&nodes, |n| n.knockout_shape),
|
||||
ids,
|
||||
}
|
||||
}
|
||||
@@ -324,8 +362,11 @@ fn info(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
"opacity": i.opacity,
|
||||
"blend": i.blend.map(BlendMode::label),
|
||||
"isolate": i.isolate,
|
||||
"knockout": i.knockout,
|
||||
"knockout": i.knockout.map(Knockout::label),
|
||||
"knockoutShape": i.knockout_shape,
|
||||
"editingMask": st.doc.mask_edit.map(|me| me.object.0),
|
||||
"pageIsolatedBlending": st.doc.page_isolate,
|
||||
"pageKnockoutGroup": st.doc.page_knockout,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Transparency",
|
||||
["Window", "Transparency"],
|
||||
None,
|
||||
"{ids?|id?, opacity?: 0..100, blend?: name, isolate?, knockout?, item?: appearance item index|null} for `ids`, the selection, or the object whose opacity mask is being edited; opacity and blend go to the targeted fill/stroke item (omitted: the Appearance panel's active item, else the objects)",
|
||||
"{ids?|id?, opacity?: 0..100, blend?: name, isolate?, knockout?: \"on\"|\"off\"|\"neutral\"|bool (true = on, false = neutral), knockoutShape?: bool, item?: appearance item index|null} for `ids`, the selection, or the object whose opacity mask is being edited; opacity and blend go to the targeted fill/stroke item (omitted: the Appearance panel's active item, else the objects)",
|
||||
has_doc,
|
||||
transparency
|
||||
),
|
||||
@@ -447,7 +447,7 @@ fn transparency(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let ItemTarget::Item { index, explicit } = item else {
|
||||
return s.execute("object.setProps", &Value::Object(q));
|
||||
};
|
||||
// Opacity and blend belong to the targeted fill/stroke; isolate/knockout stay object-level.
|
||||
// Opacity and blend belong to the targeted fill/stroke; isolate/knockout/knockoutShape stay object-level.
|
||||
let item_ids = if editing_mask { ids } else { appearance_targets(s, p)? };
|
||||
if explicit {
|
||||
let d = &s.doc()?.doc;
|
||||
@@ -461,6 +461,11 @@ fn transparency(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
{
|
||||
return Err(bad(C, format!("unknown blend mode {b}")));
|
||||
}
|
||||
if let Some(k) = q.get("knockout")
|
||||
&& vectorcraft_doc::Knockout::from_value(k).is_none()
|
||||
{
|
||||
return Err(bad(C, format!("unknown knockout state {k}")));
|
||||
}
|
||||
if opacity.is_some() || blend.is_some() {
|
||||
let ids: Vec<u64> = item_ids.iter().map(|id| id.0).collect();
|
||||
s.execute("appearance.setItem", &json!({ "index": index, "ids": ids, "opacity": opacity, "blend": blend }))?;
|
||||
|
||||
@@ -48,7 +48,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"List Graphic Styles",
|
||||
[],
|
||||
None,
|
||||
"{} → {styles: [{id, name, fill, stroke, strokeWidth, fills, strokes, effects: [effect id…], opacity: 0..100, blend, isolate, knockout, linked: [object ids]}], selected: the style the first selected object is linked to, or null}",
|
||||
"{} → {styles: [{id, name, fill, stroke, strokeWidth, fills, strokes, effects: [effect id…], opacity: 0..100, blend, isolate, knockout: \"on\"|\"off\"|\"neutral\", linked: [object ids]}], selected: the style the first selected object is linked to, or null}",
|
||||
has_doc,
|
||||
style_list
|
||||
),
|
||||
@@ -342,7 +342,7 @@ fn style_list(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
"opacity": (g.opacity * 100.0).round(),
|
||||
"blend": g.blend.label(),
|
||||
"isolate": g.isolate,
|
||||
"knockout": g.knockout,
|
||||
"knockout": g.knockout.label(),
|
||||
"linked": links.get(&g.id).map(|v| v.iter().map(|i| i.0).collect::<Vec<_>>()).unwrap_or_default(),
|
||||
})
|
||||
})
|
||||
|
||||
@@ -754,6 +754,8 @@ mod tests_file;
|
||||
#[cfg(test)]
|
||||
mod tests_gradient;
|
||||
#[cfg(test)]
|
||||
mod tests_knockout;
|
||||
#[cfg(test)]
|
||||
mod tests_live;
|
||||
#[cfg(test)]
|
||||
mod tests_menucmds;
|
||||
|
||||
@@ -384,7 +384,7 @@ fn item_transparency_is_one_step_and_checks_the_blend_mode() {
|
||||
let before = steps(&s);
|
||||
run(&mut s, "transparency.set", json!({"knockout": true}));
|
||||
assert_eq!(steps(&s), before + 1, "isolate/knockout alone do not touch the item");
|
||||
assert!(node(&s, id).knockout);
|
||||
assert_eq!(node(&s, id).knockout, vectorcraft_doc::Knockout::On);
|
||||
assert!(s.execute("transparency.set", &json!({"blend": "nope"})).is_err());
|
||||
assert_eq!(stroke(&node(&s, id), 1).blend, BlendMode::Normal);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
//! Knockout groups, the knockout shape and the page group (M3.46–M3.48): commands, undo, files
|
||||
//! (old bools load) and the PDF and SVG output.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::Knockout;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn session() -> Session {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 200, "height": 200})).unwrap();
|
||||
s
|
||||
}
|
||||
|
||||
fn run(s: &mut Session, id: &str, p: Value) -> Value {
|
||||
s.execute(id, &p).unwrap()
|
||||
}
|
||||
|
||||
fn node(s: &Session, id: u64) -> vectorcraft_doc::Node {
|
||||
s.doc().unwrap().doc.node(NodeId(id)).unwrap().clone()
|
||||
}
|
||||
|
||||
fn steps(s: &Session) -> usize {
|
||||
s.doc().unwrap().history.undo.len()
|
||||
}
|
||||
|
||||
/// A group of two overlapping 50% rectangles, selected. Returns the group id.
|
||||
fn knockout_group(s: &mut Session) -> u64 {
|
||||
let a = run(s, "shape.rectangle", json!({"x": 10, "y": 10, "width": 100, "height": 100}))["id"].clone();
|
||||
let b = run(s, "shape.rectangle", json!({"x": 60, "y": 60, "width": 100, "height": 100}))["id"].clone();
|
||||
run(s, "transparency.set", json!({"ids": [a, b], "opacity": 50}));
|
||||
run(s, "select.set", json!({"ids": [a, b]}));
|
||||
run(s, "object.group", json!({}))["id"].as_u64().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn knockout_takes_three_states_names_and_bools() {
|
||||
let mut s = session();
|
||||
let g = knockout_group(&mut s);
|
||||
assert_eq!(node(&s, g).knockout, Knockout::Neutral, "new groups are neutral");
|
||||
for (param, want) in [
|
||||
(json!("on"), Knockout::On),
|
||||
(json!("Off"), Knockout::Off),
|
||||
(json!("neutral"), Knockout::Neutral),
|
||||
(json!(true), Knockout::On),
|
||||
(json!(false), Knockout::Neutral),
|
||||
] {
|
||||
let before = steps(&s);
|
||||
run(&mut s, "transparency.set", json!({"knockout": param}));
|
||||
assert_eq!((node(&s, g).knockout, steps(&s)), (want, before + 1), "{param}");
|
||||
}
|
||||
run(&mut s, "transparency.set", json!({"knockout": "off"}));
|
||||
let info = run(&mut s, "transparency.info", json!({}));
|
||||
assert_eq!((info["knockout"].clone(), info["knockoutShape"].clone()), (json!("off"), json!(false)));
|
||||
// A bad state changes nothing, also next to an opacity (which would go to an appearance item).
|
||||
let before = steps(&s);
|
||||
assert!(s.execute("transparency.set", &json!({"knockout": "maybe"})).is_err());
|
||||
assert!(s.execute("transparency.set", &json!({"knockout": 3, "opacity": 20})).is_err());
|
||||
assert!(s.execute("object.setProps", &json!({"knockout": "maybe"})).is_err());
|
||||
assert_eq!((node(&s, g).knockout, node(&s, g).opacity, steps(&s)), (Knockout::Off, 1.0, before));
|
||||
// Undo is one step per change.
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert_eq!(node(&s, g).knockout, Knockout::Neutral);
|
||||
// The panel's click order.
|
||||
assert_eq!([Knockout::On, Knockout::Neutral, Knockout::Off].map(Knockout::cycle), [Knockout::Neutral, Knockout::Off, Knockout::On]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn knockout_shape_is_an_object_option() {
|
||||
let mut s = session();
|
||||
let g = knockout_group(&mut s);
|
||||
let child = node(&s, g).children().unwrap()[1].id.0;
|
||||
run(&mut s, "transparency.set", json!({"ids": [child], "knockoutShape": true}));
|
||||
assert!(node(&s, child).knockout_shape && !node(&s, child).has_default_transparency());
|
||||
let info = run(&mut s, "transparency.info", json!({"id": child}));
|
||||
assert_eq!(info["knockoutShape"], true);
|
||||
let mixed = run(&mut s, "transparency.info", json!({"ids": [child, g]}));
|
||||
assert_eq!(mixed["knockoutShape"], Value::Null);
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert!(!node(&s, child).knockout_shape);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_group_toggles_are_undoable_document_settings() {
|
||||
let mut s = session();
|
||||
let doc = |s: &Session| (s.doc().unwrap().doc.page_isolate, s.doc().unwrap().doc.page_knockout);
|
||||
let before = steps(&s);
|
||||
assert_eq!(run(&mut s, "transparency.togglePageIsolatedBlending", json!({})), json!({"value": true}));
|
||||
assert_eq!(run(&mut s, "transparency.togglePageKnockoutGroup", json!({"value": true})), json!({"value": true}));
|
||||
assert_eq!((doc(&s), steps(&s)), ((true, true), before + 2));
|
||||
// Setting the current value is not a change.
|
||||
run(&mut s, "transparency.togglePageKnockoutGroup", json!({"value": true}));
|
||||
assert_eq!(steps(&s), before + 2);
|
||||
let info = run(&mut s, "transparency.info", json!({}));
|
||||
assert_eq!((info["pageIsolatedBlending"].clone(), info["pageKnockoutGroup"].clone()), (json!(true), json!(true)));
|
||||
run(&mut s, "edit.undo", json!({}));
|
||||
assert_eq!(doc(&s), (true, false));
|
||||
run(&mut s, "transparency.togglePageIsolatedBlending", json!({}));
|
||||
assert_eq!(doc(&s), (false, false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn knockout_settings_round_trip_and_old_bools_load() {
|
||||
let mut s = session();
|
||||
let g = knockout_group(&mut s);
|
||||
let child = node(&s, g).children().unwrap()[0].id.0;
|
||||
run(&mut s, "transparency.set", json!({"ids": [g], "knockout": "off"}));
|
||||
run(&mut s, "transparency.set", json!({"ids": [child], "knockout": "on", "knockoutShape": true}));
|
||||
run(&mut s, "transparency.togglePageIsolatedBlending", json!({}));
|
||||
run(&mut s, "transparency.togglePageKnockoutGroup", json!({}));
|
||||
let bytes = vectorcraft_format::save(&s.doc().unwrap().doc, false);
|
||||
let back = vectorcraft_format::load(&bytes).unwrap();
|
||||
let (bg, bc) = (back.node(NodeId(g)).unwrap(), back.node(NodeId(child)).unwrap());
|
||||
assert_eq!((bg.knockout, bc.knockout, bc.knockout_shape, back.page_isolate, back.page_knockout), (Knockout::Off, Knockout::On, true, true, true));
|
||||
// Defaults are not written.
|
||||
let text = String::from_utf8(vectorcraft_format::save(&session().doc().unwrap().doc, false)).unwrap();
|
||||
assert!(!text.contains("knockout") && !text.contains("page_isolate"), "{text}");
|
||||
// Files from before the three states stored a bool: true is On, false is Neutral.
|
||||
let mut v: Value = serde_json::from_slice(&bytes).unwrap();
|
||||
let mut patched = 0;
|
||||
fn patch(v: &mut Value, n: &mut usize) {
|
||||
match v {
|
||||
Value::Object(m) => {
|
||||
if let Some(k) = m.get_mut("knockout") {
|
||||
*k = json!(*k == json!("on"));
|
||||
*n += 1;
|
||||
}
|
||||
m.values_mut().for_each(|c| patch(c, n));
|
||||
}
|
||||
Value::Array(a) => a.iter_mut().for_each(|c| patch(c, n)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
patch(&mut v, &mut patched);
|
||||
assert_eq!(patched, 2);
|
||||
let old = vectorcraft_format::load(&serde_json::to_vec(&v).unwrap()).unwrap();
|
||||
assert_eq!((old.node(NodeId(g)).unwrap().knockout, old.node(NodeId(child)).unwrap().knockout), (Knockout::Neutral, Knockout::On));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pdf_and_svg_write_knockout_groups_as_masks() {
|
||||
let mut s = session();
|
||||
let g = knockout_group(&mut s);
|
||||
let plain = s.doc().unwrap().doc.clone();
|
||||
run(&mut s, "transparency.set", json!({"ids": [g], "knockout": "on"}));
|
||||
let ko = s.doc().unwrap().doc.clone();
|
||||
|
||||
let pdf = |d: &Document| vectorcraft_pdf::export_with_report(d, &vectorcraft_pdf::PdfOptions { compress: false, ..Default::default() }).unwrap();
|
||||
let r = pdf(&plain);
|
||||
assert!(!String::from_utf8_lossy(&r.bytes).contains("/SMask") && r.warnings.is_empty());
|
||||
let r = pdf(&ko);
|
||||
let text = String::from_utf8_lossy(&r.bytes);
|
||||
// The lower rectangle is drawn through a luminosity mask made of the upper one's alpha.
|
||||
assert!(text.contains("/Luminosity") && text.contains("/Alpha"), "soft masks");
|
||||
assert!(r.warnings.iter().any(|w| w.contains("knockout")), "{:?}", r.warnings);
|
||||
|
||||
let svg = |d: &Document| vectorcraft_svg::export(d, &Default::default());
|
||||
assert!(!svg(&plain).contains("knockout"));
|
||||
let out = svg(&ko);
|
||||
assert_eq!(out.matches("mask=\"url(#knockout-").count(), 1, "{out}");
|
||||
assert!(out.contains("<feColorMatrix") && out.contains("isolation"), "{out}");
|
||||
|
||||
// A neutral group passes its children through: still one mask per element below the top one.
|
||||
run(&mut s, "select.set", json!({"ids": [g]}));
|
||||
let outer = run(&mut s, "object.group", json!({}))["id"].as_u64().unwrap();
|
||||
run(&mut s, "transparency.set", json!({"ids": [g], "knockout": "neutral"}));
|
||||
run(&mut s, "transparency.set", json!({"ids": [outer], "knockout": "on"}));
|
||||
assert_eq!(svg(&s.doc().unwrap().doc).matches("mask=\"url(#knockout-").count(), 1);
|
||||
|
||||
// The page group: an isolated group around the page content.
|
||||
let mut page = (*plain).clone();
|
||||
page.page_isolate = true;
|
||||
assert!(svg(&page).contains("isolation"));
|
||||
page.page_knockout = true;
|
||||
assert_eq!(svg(&page).matches("mask=\"url(#knockout-").count(), 1, "the layer is neutral: its objects knock each other out");
|
||||
assert!(String::from_utf8_lossy(&pdf(&page).bytes).contains("/Luminosity"));
|
||||
}
|
||||
@@ -180,11 +180,14 @@ fn info_reports_mixed_values_as_null() {
|
||||
s.execute("transparency.set", &json!({"ids": [a.0, b.0], "blend": "Screen"})).unwrap();
|
||||
s.execute("select.set", &json!({"ids": [a.0, b.0]})).unwrap();
|
||||
let info = s.execute("transparency.info", &json!({})).unwrap();
|
||||
assert_eq!(info, json!({"ids": [a.0, b.0], "opacity": null, "blend": "Screen", "isolate": null, "knockout": false, "editingMask": null}));
|
||||
assert_eq!(
|
||||
info,
|
||||
json!({"ids": [a.0, b.0], "opacity": null, "blend": "Screen", "isolate": null, "knockout": "neutral", "knockoutShape": false, "editingMask": null, "pageIsolatedBlending": false, "pageKnockoutGroup": false})
|
||||
);
|
||||
let one = s.execute("transparency.info", &json!({"id": a.0})).unwrap();
|
||||
assert_eq!((one["opacity"].clone(), one["isolate"].clone()), (json!(50.0), json!(true)));
|
||||
let i = s.transparency_info();
|
||||
assert_eq!((i.opacity, i.blend, i.knockout), (None, Some(vectorcraft_color::BlendMode::Screen), Some(false)));
|
||||
assert_eq!((i.opacity, i.blend, i.knockout), (None, Some(vectorcraft_color::BlendMode::Screen), Some(vectorcraft_doc::Knockout::Neutral)));
|
||||
s.execute("select.none", &json!({})).unwrap();
|
||||
assert_eq!(s.execute("transparency.info", &json!({})).unwrap()["opacity"], Value::Null);
|
||||
// Many targets are gathered in one walk of the tree.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_color::{BlendMode, Color, Paint};
|
||||
use vectorcraft_doc::{GraphicStyle, Node};
|
||||
use vectorcraft_doc::{GraphicStyle, Knockout, Node};
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -53,11 +53,11 @@ fn styles_carry_opacity_and_blend_through_a_save() {
|
||||
run(&mut s, "transparency.set", json!({"item": null, "opacity": 50, "blend": "multiply", "knockout": true}));
|
||||
run(&mut s, "graphicStyle.new", json!({"name": "Half"}));
|
||||
let g = style(&s, "Half");
|
||||
assert_eq!((g.opacity, g.blend, g.knockout), (0.5, BlendMode::Multiply, true));
|
||||
assert_eq!((g.opacity, g.blend, g.knockout), (0.5, BlendMode::Multiply, Knockout::On));
|
||||
let b = rect(&mut s, 100.0);
|
||||
run(&mut s, "graphicStyle.apply", json!({"name": "Half"}));
|
||||
let nb = node(&s, b);
|
||||
assert_eq!((nb.opacity, nb.blend, nb.knockout), (0.5, BlendMode::Multiply, true));
|
||||
assert_eq!((nb.opacity, nb.blend, nb.knockout), (0.5, BlendMode::Multiply, Knockout::On));
|
||||
let l = run(&mut s, "graphicStyle.list", json!({}));
|
||||
let half = l["styles"].as_array().unwrap().iter().find(|g| g["name"] == "Half").unwrap();
|
||||
assert_eq!((half["opacity"].as_f64(), half["blend"].as_str()), (Some(50.0), Some("Multiply")));
|
||||
|
||||
Reference in New Issue
Block a user