mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-28 03:37:00 -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(),
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user