Merge pull request #4621 from storytold/feat/compound-shapes

Pathfinder: live compound shapes (Make, Shape Mode, Release, Expand Compound Shape)
This commit is contained in:
BFlat
2026-10-30 00:44:58 +07:00
committed by GitHub
48 changed files with 946 additions and 79 deletions
+76
View File
@@ -0,0 +1,76 @@
//! Compound shapes (Pathfinder panel › Make Compound Shape): a group whose members each have a
//! shape mode. It draws as the one area the modes make, from the backmost member up, painted with
//! the group's own appearance; the members stay editable. Evaluated in `vectorcraft-effects`.
use serde::{Deserialize, Deserializer, Serialize};
use crate::{Node, NodeKind};
/// How a compound shape's member combines with the area of the members below it. The backmost
/// member's mode does not matter: it starts the area.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum ShapeMode {
/// Adds the member's area.
#[default]
Add,
/// Takes the member's area away.
Subtract,
/// Keeps only what the member's area overlaps.
Intersect,
/// Keeps where exactly one of the area and the member's area is.
Exclude,
}
impl ShapeMode {
/// Every mode, in the Pathfinder panel's order.
pub const ALL: [ShapeMode; 4] = [ShapeMode::Add, ShapeMode::Subtract, ShapeMode::Intersect, ShapeMode::Exclude];
/// The mode's name in commands and files.
pub fn name(self) -> &'static str {
match self {
ShapeMode::Add => "add",
ShapeMode::Subtract => "subtract",
ShapeMode::Intersect => "intersect",
ShapeMode::Exclude => "exclude",
}
}
/// The mode named `s` ([`ShapeMode::name`]).
pub fn parse(s: &str) -> Option<Self> {
Self::ALL.into_iter().find(|m| m.name() == s)
}
}
/// Files are untrusted: a mode that isn't one of the names reads as Add.
impl<'de> Deserialize<'de> for ShapeMode {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let v = serde_json::Value::deserialize(d)?;
Ok(v.as_str().and_then(ShapeMode::parse).unwrap_or_default())
}
}
impl Node {
/// Is this a compound shape? Only a group that doesn't clip can be one: the mark on anything
/// else (from a file) is ignored.
pub fn is_compound_shape(&self) -> bool {
matches!(self.kind, NodeKind::Group { clip: false, .. }) && self.extras().compound_shape
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn modes_read_back_and_unknown_ones_are_add() {
for m in ShapeMode::ALL {
let v = serde_json::to_value(m).unwrap();
assert_eq!(v, serde_json::json!(m.name()));
assert_eq!(serde_json::from_value::<ShapeMode>(v).unwrap(), m);
}
for v in [serde_json::json!("divide"), serde_json::json!(3), serde_json::json!(null), serde_json::json!({"a": [1]})] {
assert_eq!(serde_json::from_value::<ShapeMode>(v).unwrap(), ShapeMode::Add);
}
}
}
+2
View File
@@ -11,6 +11,7 @@ pub mod blend;
pub mod cancel;
pub mod clipnest;
pub mod cmyk;
pub mod compound_shape;
pub mod corners;
pub mod depth;
pub mod dimension;
@@ -76,6 +77,7 @@ pub use appearance::{
StrokeAlign, StrokeLayer, WidthProfile,
};
pub use assets::ExportAsset;
pub use compound_shape::ShapeMode;
pub use corners::LiveCorners;
pub use depth::{MAX_DEPTH, MAX_SYMBOL_NODES};
pub use graph::{
+7
View File
@@ -53,6 +53,11 @@ pub struct NodeExtras {
/// colorizes it. Each pixel is drawn in this color, as opaque as the pixel is dark, so white
/// pixels are transparent. `None`: the image shows its own pixels.
pub colorize: Option<Box<vectorcraft_color::Paint>>,
/// Pathfinder panel › Make Compound Shape: this group is a compound shape
/// ([`Node::is_compound_shape`], [`crate::compound_shape`]).
pub compound_shape: bool,
/// As a member of a compound shape, how this object combines with the members below it.
pub shape_mode: crate::ShapeMode,
}
/// No extras: what [`Node::extras`] gives for a node without any.
@@ -74,6 +79,8 @@ pub(super) static NO_EXTRAS: NodeExtras = NodeExtras {
live_paint_gaps: None,
objects_on_path: None,
colorize: None,
compound_shape: false,
shape_mode: crate::ShapeMode::Add,
};
/// A node's [`NodeExtras`], if it has any ([`Node::extras`]). Extras that are all unset equal
+1
View File
@@ -127,6 +127,7 @@ impl Node {
NodeKind::Layer { .. } => "Layer",
NodeKind::Group { clip: true, .. } => "Clip Group",
NodeKind::Group { .. } if self.extras().objects_on_path.is_some() => "Objects on Path",
NodeKind::Group { .. } if self.extras().compound_shape => "Compound Shape",
NodeKind::Group { .. } => "Group",
NodeKind::Path { live: Some(l), .. } => l.label(),
NodeKind::Path { guide: true, .. } => "Guide",
+5
View File
@@ -19,6 +19,8 @@ fn extras_are_written_flat_in_order() {
e.slice = Some(Box::default());
e.perspective = Some(Box::new(crate::PerspectiveAttachment::new("left", 1.0)));
e.live_paint_gaps = Some(Box::default());
e.compound_shape = true;
e.shape_mode = crate::ShapeMode::Exclude;
}
/// The top-level keys of a JSON object, in the order written.
struct Keys(Vec<String>);
@@ -61,8 +63,11 @@ fn extras_are_written_flat_in_order() {
"bbox_angle",
"perspective",
"live_paint_gaps",
"compound_shape",
"shape_mode",
];
assert_eq!(keys, order);
assert_eq!(v["shape_mode"], "exclude");
assert_eq!(serde_json::from_value::<Node>(v).unwrap(), n);
let plain = Node::path(NodeId(9), shapes::rectangle(Rect::new(0.0, 0.0, 4.0, 2.0)), Appearance::default_art());
let back: Node = serde_json::from_value(serde_json::to_value(&plain).unwrap()).unwrap();
+17 -5
View File
@@ -5,6 +5,12 @@ use super::kind::NodeKind;
use super::opacity::{Knockout, OpacityMask};
use super::{Appearance, BlendMode, Deserialize, Node, PathData, Serialize, one, yes};
/// Field `name` of a [`Node`] written as `v`, or skipped. A function of its own, so the long field
/// list stays one straight run of calls.
fn write_field<S: serde::ser::SerializeStruct, T: Serialize + ?Sized>(st: &mut S, skip: bool, name: &'static str, v: &T) -> Result<(), S::Error> {
if skip { st.skip_field(name) } else { st.serialize_field(name, v) }
}
/// How a [`Node`] is written: the flat field list of the file format, in its order, borrowed from
/// the node and its extras (nothing is copied). The node reads back through `NodeWire`.
impl Serialize for Node {
@@ -43,16 +49,14 @@ impl Serialize for Node {
e.live_paint_gaps.is_none(),
e.objects_on_path.is_none(),
e.colorize.is_none(),
is_default(&e.compound_shape),
is_default(&e.shape_mode),
];
let mut st = s.serialize_struct("Node", skips.iter().filter(|s| !**s).count())?;
let mut skip = skips.into_iter();
macro_rules! field {
($name:literal, $v:expr) => {
if skip.next().unwrap_or(false) {
st.skip_field($name)?;
} else {
st.serialize_field($name, $v)?;
}
write_field(&mut st, skip.next().unwrap_or(false), $name, $v)?;
};
}
field!("id", &self.id);
@@ -84,6 +88,8 @@ impl Serialize for Node {
field!("live_paint_gaps", &e.live_paint_gaps);
field!("objects_on_path", &e.objects_on_path);
field!("colorize", &e.colorize);
field!("compound_shape", &e.compound_shape);
field!("shape_mode", &e.shape_mode);
st.end()
}
}
@@ -186,6 +192,10 @@ pub(super) struct NodeWire {
objects_on_path: Option<Box<crate::objects_on_path::ObjectsOnPath>>,
#[serde(default)]
colorize: Option<Box<vectorcraft_color::Paint>>,
#[serde(default)]
compound_shape: bool,
#[serde(default)]
shape_mode: crate::ShapeMode,
}
impl From<NodeWire> for Node {
@@ -208,6 +218,8 @@ impl From<NodeWire> for Node {
live_paint_gaps: w.live_paint_gaps,
objects_on_path: w.objects_on_path,
colorize: w.colorize,
compound_shape: w.compound_shape,
shape_mode: w.shape_mode,
};
Node {
id: w.id,
+93 -9
View File
@@ -7,6 +7,10 @@
//! result keeps the appearance of the object whose paint it takes. Trap ([`crate::trap`]) keeps the
//! children and adds its traps above them.
//!
//! **Compound shapes** ([`compound_shape_area`]): a compound shape draws as the one area its
//! members' shape modes make, painted by its own appearance as a group's own fills and strokes
//! paint its members.
//!
//! **Container appearance** ([`evaluate_container`]): a group's or layer's own fills and strokes
//! paint every member's geometry, below or above the members as its contents slot says
//! ([`Appearance::contents_at`]); its geometry effects reshape the members as one piece; its raster
@@ -16,7 +20,7 @@ use std::sync::Arc;
use serde_json::Value;
use vectorcraft_doc::color::{Color, Paint};
use vectorcraft_doc::{Appearance, AppearanceItem, Document, Node, NodeId, NodeKind, StrokeLayer};
use vectorcraft_doc::{Appearance, AppearanceItem, Document, Node, NodeId, NodeKind, ShapeMode, StrokeLayer};
use vectorcraft_geom::{FillRule, PathData, Rect};
use vectorcraft_pathops::{self as po, PathfinderOp};
@@ -45,10 +49,11 @@ fn op_of(id: &str) -> Option<PathfinderOp> {
PATHFINDER_EFFECTS.iter().find(|e| e.0 == id).map(|e| e.2)
}
/// Does `n` (a group or layer) carry a visible Pathfinder effect?
/// Does `n` (a group or layer) carry a visible Pathfinder effect, or is it a compound shape?
pub fn has_pathfinder(n: &Node) -> bool {
matches!(n.kind, NodeKind::Group { clip: false, .. } | NodeKind::Layer { .. })
&& n.appearance.effects.iter().any(|e| e.visible && is_pathfinder(&e.id))
n.is_compound_shape()
|| (matches!(n.kind, NodeKind::Group { clip: false, .. } | NodeKind::Layer { .. })
&& n.appearance.effects.iter().any(|e| e.visible && is_pathfinder(&e.id)))
}
/// Converts objects that aren't paths (e.g. text) to outline art, if possible. The paths and
@@ -62,11 +67,15 @@ fn leaves(n: &Node, hook: Option<OutlineHook>, out: &mut Vec<Node>) {
}
match &n.kind {
NodeKind::Path { guide: false, .. } | NodeKind::Compound { .. } => out.push(n.clone()),
NodeKind::Group { children, .. } | NodeKind::Layer { children, .. } => {
for c in children {
leaves(c, hook, out);
// A compound shape takes part as its area, styled like it.
NodeKind::Group { children, .. } | NodeKind::Layer { children, .. } => match compound_shape_art(n, hook) {
Some(art) => out.push(art),
None => {
for c in children {
leaves(c, hook, out);
}
}
}
},
_ => {
// Every path of the outline art, at any depth, takes the object's opacity and blend
// mode.
@@ -91,6 +100,63 @@ fn geometry(n: &Node) -> Option<(PathData, FillRule)> {
}
}
/// The operation that combines a compound shape's member in `mode` with the area below it.
fn shape_op(mode: ShapeMode) -> po::BoolOp {
match mode {
ShapeMode::Add => po::BoolOp::Union,
ShapeMode::Subtract => po::BoolOp::Difference,
ShapeMode::Intersect => po::BoolOp::Intersect,
ShapeMode::Exclude => po::BoolOp::Xor,
}
}
/// The area compound shape `n` draws: each visible member's shape (a group's leaves together, type
/// as its outlines through `hook`, open paths closed) combined with the area of the members below
/// it by the member's shape mode; the backmost member starts the area. `None` when `n` is not a
/// compound shape, or its members are past a live Pathfinder's budget ([`PATHFINDER_MAX_SEGMENTS`],
/// [`PATHFINDER_MAX_OVERLAPS`]).
pub fn compound_shape_area(n: &Node, hook: Option<OutlineHook>) -> Option<PathData> {
if !n.is_compound_shape() {
return None;
}
let mut all: Vec<Node> = vec![];
// Each member that has a shape: its operation and how many of the leaves are its.
let mut members: Vec<(po::BoolOp, usize)> = vec![];
for c in n.children()? {
let before = all.len();
leaves(c, hook, &mut all);
if all.len() > before {
members.push((shape_op(c.extras().shape_mode), all.len() - before));
}
}
if !within_budget(&all) {
return None;
}
if all.is_empty() {
return Some(PathData::default());
}
let parts: Vec<(PathData, FillRule)> = all.iter().map(|l| geometry(l).unwrap_or_default()).collect();
let refs: Vec<(&PathData, FillRule)> = parts.iter().map(|(p, r)| (p, *r)).collect();
Some(po::boolean_n(&refs, |inside| {
let (mut start, mut area) = (0, false);
for (k, (op, count)) in members.iter().enumerate() {
let here = inside.get(start..start + count).is_some_and(|flags| flags.contains(&true));
start += count;
area = if k == 0 { here } else { op.apply(area, here) };
}
area
}))
}
/// Compound shape `n` as one path of its area ([`compound_shape_area`]) with its appearance,
/// opacity and blend mode: how it takes part in a Pathfinder operation.
pub fn compound_shape_art(n: &Node, hook: Option<OutlineHook>) -> Option<Node> {
let area = compound_shape_area(n, hook)?;
let mut art = Node::path(n.id, area, Appearance { items: n.appearance.items.clone(), ..Default::default() });
(art.opacity, art.blend) = (n.opacity, n.blend);
Some(art)
}
/// A result path styled like `src` (its appearance, opacity and blend mode).
fn styled(src: &Node, path: PathData, outline: bool) -> Node {
let mut appearance = Appearance { items: src.appearance.items.clone(), ..Default::default() };
@@ -196,7 +262,16 @@ pub fn pathfinder_children(group: &Node, hook: Option<OutlineHook>) -> Option<Ve
return None;
}
let source: &[Arc<Node>] = group.children()?;
let mut stack: Vec<Arc<Node>> = source.to_vec();
let mut stack: Vec<Arc<Node>> = if group.is_compound_shape() {
// The group's own appearance paints the area ([`evaluate_container`]), so the art has none.
match compound_shape_area(group, hook) {
Some(area) if area.is_empty() => vec![],
Some(area) => vec![Arc::new(Node::path(group.id, area, Appearance::default()))],
None => return Some(source.to_vec()),
}
} else {
source.to_vec()
};
for e in group.appearance.effects.iter().filter(|e| e.visible) {
if e.id == crate::trap::TRAP {
let mut lv = vec![];
@@ -375,6 +450,13 @@ pub fn member_shapes(doc: Option<&Document>, n: &Node) -> Vec<(PathData, FillRul
}
match &n.kind {
NodeKind::Path { guide: false, .. } | NodeKind::Compound { .. } => out.extend(crate::reshape::leaf_geometry(n)),
NodeKind::Group { .. } if n.is_compound_shape() => {
let hook: &dyn Fn(&Node) -> Option<Node> = &crate::outline_text;
match compound_shape_area(n, Some(hook)) {
Some(area) => out.push((area, FillRule::NonZero)),
None => members(doc, n, out),
}
}
NodeKind::Text(t) => {
let (_, all) = crate::reshape::glyph_outlines(t);
if !all.elements().is_empty() {
@@ -454,6 +536,8 @@ pub fn evaluate_container(doc: Option<&Document>, n: &Node) -> Option<Node> {
if let Some(ch) = m.children_mut() {
*ch = result;
}
// The area is art now: drawn again, it is no compound shape of its own.
m.extras_mut().compound_shape = false;
}
if !fx.is_empty() {
m = crate::reshape::outlined_members(doc, &m);
+69
View File
@@ -347,3 +347,72 @@ fn pathfinder_on_many_stacked_bars_still_runs() {
let out = pathfinder_children(&g, None).unwrap();
assert_eq!(out.len(), 1);
}
/// Two overlapping squares (0–10 and 5–15 by 10 high) as a compound shape, the front one in `mode`.
fn compound_shape(mode: ShapeMode) -> Node {
let mut front = (*sq(2, 5.0, Color::rgb(0.0, 0.0, 1.0))).clone();
front.extras_mut().shape_mode = mode;
let mut g = Node::group(NodeId(10), vec![sq(1, 0.0, Color::rgb(1.0, 0.0, 0.0)), Arc::new(front)]);
g.extras_mut().compound_shape = true;
g.appearance = Appearance::basic(Paint::solid(Color::rgb(0.0, 1.0, 0.0)), Paint::None, 0.0).into();
g
}
/// A compound shape draws the area its members' modes make, one unpainted path that its own
/// appearance paints; evaluated, it is plain art painted with that appearance.
#[test]
fn compound_shapes_draw_the_area_their_shape_modes_make() {
for (mode, want) in [(ShapeMode::Add, 150.0), (ShapeMode::Subtract, 50.0), (ShapeMode::Intersect, 50.0), (ShapeMode::Exclude, 100.0)] {
let g = compound_shape(mode);
assert!(has_pathfinder(&g), "{mode:?}");
let area_path = compound_shape_area(&g, None).unwrap();
assert!((po::area(&area_path, FillRule::NonZero).abs() - want).abs() < 0.01, "{mode:?}");
let out = pathfinder_children(&g, None).unwrap();
assert_eq!(out.len(), 1, "{mode:?}");
assert!(out[0].appearance.items.is_empty(), "the group's own appearance paints it");
let art = evaluate_container(None, &g).unwrap();
assert!(!art.is_compound_shape(), "evaluated, it is art");
let painted: Vec<&Arc<Node>> = art.children().unwrap().iter().filter(|c| !c.appearance.items.is_empty()).collect();
assert_eq!(painted.len(), 1);
assert_eq!(painted[0].appearance.fill_paint().color(), Some(Color::rgb(0.0, 1.0, 0.0)), "{mode:?}");
assert!((area(painted[0]) - want).abs() < 0.01, "{mode:?}");
}
// The backmost member's mode doesn't change the area.
let mut g = compound_shape(ShapeMode::Add);
if let Some(ch) = g.children_mut() {
let mut back = (*ch[0]).clone();
back.extras_mut().shape_mode = ShapeMode::Subtract;
ch[0] = Arc::new(back);
}
assert!((po::area(&compound_shape_area(&g, None).unwrap(), FillRule::NonZero).abs() - 150.0).abs() < 0.01);
// Hidden members take no part; the mark on a clip group is ignored.
let mut g = compound_shape(ShapeMode::Subtract);
if let Some(ch) = g.children_mut() {
let mut front = (*ch[1]).clone();
front.visible = false;
ch[1] = Arc::new(front);
}
assert!((po::area(&compound_shape_area(&g, None).unwrap(), FillRule::NonZero).abs() - 100.0).abs() < 0.01);
let mut clip = compound_shape(ShapeMode::Add);
clip.kind = NodeKind::Group { children: clip.children().unwrap().clone(), clip: true };
assert!(!clip.is_compound_shape() && compound_shape_area(&clip, None).is_none());
}
/// A compound shape inside another (and inside an Effect › Pathfinder group) takes part as its area,
/// styled like it.
#[test]
fn nested_compound_shapes_take_part_as_their_area() {
let inner = compound_shape(ShapeMode::Subtract);
let mut cutter = (*sq(3, 2.0, Color::BLACK)).clone();
cutter.extras_mut().shape_mode = ShapeMode::Intersect;
let mut outer = Node::group(NodeId(20), vec![Arc::new(inner.clone()), Arc::new(cutter)]);
outer.extras_mut().compound_shape = true;
// (0–5 of the inner area) ∩ (2–12) = 2–5 by 10 high.
assert!((po::area(&compound_shape_area(&outer, None).unwrap(), FillRule::NonZero).abs() - 30.0).abs() < 0.01);
let mut effect = Node::group(NodeId(30), vec![Arc::new(inner)]);
effect.appearance.effects.push(Effect { id: "pathfinder.add".into(), params: serde_json::json!({}), visible: true });
let out = pathfinder_children(&effect, None).unwrap();
assert_eq!(out.len(), 1);
assert!((area(&out[0]) - 50.0).abs() < 0.01);
assert_eq!(out[0].appearance.fill_paint().color(), Some(Color::rgb(0.0, 1.0, 0.0)), "styled like the compound shape");
}
+2 -2
View File
@@ -69,8 +69,8 @@ pub use adjust::{
pub use bake::{StrokeArt, bake_appearance, bake_document, expand_art, expand_leaf, fresh_ids, needs_bake};
pub use clip::clip_outline;
pub use group::{
OutlineHook, PATHFINDER_EFFECTS, evaluate_container, has_container_appearance, has_pathfinder, is_pathfinder, member_shapes, paints,
pathfinder_children,
OutlineHook, PATHFINDER_EFFECTS, compound_shape_area, compound_shape_art, evaluate_container, has_container_appearance, has_pathfinder,
is_pathfinder, member_shapes, paints, pathfinder_children,
};
pub use lighting_presets::{LIGHTING_PRESETS, LightingPreset, lighting_preset_params, matching_lighting_preset};
pub use live::{expand_live, expand_live_deep, expanded_live_group, text_outliner};
+16 -7
View File
@@ -10,6 +10,7 @@ pub mod clipboard;
mod colorcmds;
pub mod colormgmt;
pub mod colortheme;
mod compoundshape;
pub(crate) mod create;
mod css;
mod cut;
@@ -249,6 +250,7 @@ pub fn command_specs() -> &'static [CommandSpec] {
v.extend(xform::specs());
v.extend(effectcmd::specs());
v.extend(pathops::specs());
v.extend(compoundshape::specs());
v.extend(typecmd::specs());
v.extend(menucmds::specs());
v.extend(live::specs());
@@ -523,26 +525,30 @@ pub(crate) fn ok() -> Result<Value> {
}
/// Leaves whose appearance a paint command changes: groups and layers expand to their contents,
/// and compound paths own their children's appearance, so a member stands for its compound path
/// (#3692), which is listed once.
/// and compound paths and compound shapes own their members' appearance, so a member stands for
/// its compound path (#3692) or compound shape, which is listed once.
pub(crate) fn leaf_targets(s: &Session, ids: &[NodeId]) -> Result<Vec<NodeId>> {
use vectorcraft_doc::NodeKind;
let d = &s.doc()?.doc;
let compound = |id: NodeId| d.parent_of(id).filter(|p| matches!(d.node(*p).map(|n| &n.kind), Some(NodeKind::Compound { .. })));
let owns = |n: &vectorcraft_doc::Node| matches!(n.kind, NodeKind::Compound { .. }) || n.is_compound_shape();
let compound = |id: NodeId| d.parent_of(id).filter(|p| d.node(*p).is_some_and(owns));
let mut out = vec![];
for id in ids {
let Some(n) = d.node(*id) else { continue };
match &n.kind {
NodeKind::Group { .. } | NodeKind::Layer { .. } => n.walk(&mut |c| {
if !c.is_container() || matches!(c.kind, NodeKind::Compound { .. }) {
NodeKind::Group { .. } | NodeKind::Layer { .. } if !n.is_compound_shape() => n.walk(&mut |c| {
if !c.is_container() || owns(c) {
out.push(c.id)
}
}),
_ => out.push(compound(*id).unwrap_or(*id)),
}
}
let comp: Vec<NodeId> = out.iter().filter(|id| matches!(d.node(**id).map(|n| &n.kind), Some(NodeKind::Compound { .. }))).copied().collect();
out.retain(|id| !comp.iter().any(|c| d.parent_of(*id) == Some(*c)));
let comp: std::collections::HashSet<NodeId> = out.iter().filter(|id| d.node(**id).is_some_and(owns)).copied().collect();
if !comp.is_empty() {
// A compound shape's members lie at any depth in it.
out.retain(|id| !d.ancestry(*id).is_some_and(|up| up.iter().rev().skip(1).any(|a| comp.contains(a))));
}
let mut seen = std::collections::HashSet::new();
out.retain(|id| seen.insert(*id));
Ok(out)
@@ -590,6 +596,9 @@ pub use opacitymask::TransparencyInfo;
/// Eyedropper Options (`Prefs::eyedropper`).
pub use xform::{AppearanceAttrs, EyedropperAttrs, EyedropperOptions, FillAttrs, StrokeAttrs};
/// The shape modes of the selected compound-shape members (the Pathfinder panel lights theirs).
pub use compoundshape::selected_member_modes;
/// Pathfinder Options (`Prefs::pathfinder`).
pub use pathops::{PATHFINDER_PRECISION_RANGE, PathfinderOptions};
+256
View File
@@ -0,0 +1,256 @@
//! Pathfinder panel › compound shapes (`object.compoundShape.*`): Make Compound Shape (what
//! Alt-clicking a Shape Mode button does), a member's shape mode, Release and Expand Compound
//! Shape. A compound shape is a group marked so ([`Node::is_compound_shape`]) whose members each
//! have a [`ShapeMode`]; `vectorcraft-effects` draws it as the area they make
//! ([`effects::compound_shape_area`]).
use std::sync::Arc;
use serde_json::{Value, json};
use vectorcraft_doc::{Document, Node, NodeId, NodeKind, ShapeMode};
use vectorcraft_render::effects;
use super::edit::{roots_of, selected_roots};
use super::pathops::{pf_leaves, shape_node, style_of};
use super::{CommandSpec, EngineError, Result, Session, bad, cmd, has_multi};
const MODE_NAMES: [&str; 4] = ["add", "subtract", "intersect", "exclude"];
pub fn specs() -> Vec<CommandSpec> {
vec![
cmd!(
"object.compoundShape.make",
"Make Compound Shape",
["Window", "Pathfinder"],
None,
"{ids?: [id…] (selected first; default: the selection), mode?: \"add\"|\"subtract\"|\"intersect\"|\"exclude\" (default add; Alt-clicking a Shape Mode button gives its mode)} the two or more selected objects (paths, compound paths, groups, compound shapes, type) become the members of one compound shape, each with the shape mode, in their stacking order where the frontmost was: it draws as the area they make from the backmost up (open paths closed), painted with the appearance and transparency of the topmost member in Add, Intersect or Exclude mode (with every member in Subtract, the backmost), and the members stay editable (Direct Selection, Layers panel, isolation mode) → {id}",
has_multi,
make
),
cmd!(
"object.compoundShape.setMode",
"Shape Mode",
["Window", "Pathfinder"],
None,
"{mode: \"add\"|\"subtract\"|\"intersect\"|\"exclude\", ids?: [id…] (selected first; default: the selection)} the members of compound shapes that the selected objects are or are in take the shape mode (what clicking a Shape Mode button does with members selected); the backmost member's mode doesn't change the shape → {ids}",
has_member,
set_mode
),
cmd!(
"object.compoundShape.release",
"Release Compound Shape",
["Window", "Pathfinder"],
None,
"{ids?: [id…] (selected first; default: the selection)} the selected compound shapes (and those holding the selected objects) become their members again, as they were before Make Compound Shape, selected → {ids}",
has_compound_shape,
release
),
cmd!(
"object.compoundShape.expand",
"Expand Compound Shape",
["Window", "Pathfinder"],
None,
"{ids?: [id…] (selected first; default: the selection)} the selected compound shapes (and those holding the selected objects) become the path, or compound path, of the area they draw, painted as they are, selected → {ids}",
has_compound_shape,
expand
),
]
}
/// The innermost compound shape holding `id` (not `id` itself), with the member of it that `id` is
/// or is in.
fn holder(d: &Document, id: NodeId) -> Option<(NodeId, NodeId)> {
let up = d.ancestry(id)?;
up.windows(2).rev().find_map(|w| match w {
[shape, member] if d.node(*shape).is_some_and(Node::is_compound_shape) => Some((*shape, *member)),
_ => None,
})
}
/// The compound shape `id` is or is in (the innermost).
fn shape_of(d: &Document, id: NodeId) -> Option<NodeId> {
if d.node(id)?.is_compound_shape() { Some(id) } else { holder(d, id).map(|(shape, _)| shape) }
}
/// The compound shapes the selection is or is in, outermost only, in paint order.
fn selected_shapes(s: &Session) -> Result<Vec<NodeId>> {
let st = s.doc()?;
let mut shapes: Vec<NodeId> = vec![];
for id in st.selection.in_paint_order(&st.doc) {
if let Some(shape) = shape_of(&st.doc, id).filter(|shape| !shapes.contains(shape)) {
shapes.push(shape);
}
}
Ok(roots_of(&st.doc, shapes))
}
/// The members of compound shapes that the selected objects are or are in.
fn selected_members(s: &Session) -> Result<Vec<NodeId>> {
let st = s.doc()?;
let mut members: Vec<NodeId> = vec![];
for id in &st.selection.objects {
if let Some((_, member)) = holder(&st.doc, *id).filter(|(_, m)| !members.contains(m)) {
members.push(member);
}
}
Ok(members)
}
/// The shape modes of the compound-shape members the selection is or is in (none when it is in
/// none): the Pathfinder panel lights the Shape Mode button they share.
pub fn selected_member_modes(s: &Session) -> Vec<ShapeMode> {
let Some(st) = s.active() else { return vec![] };
let members = selected_members(s).unwrap_or_default();
members.iter().filter_map(|id| st.doc.node(*id)).map(|n| n.extras().shape_mode).collect()
}
/// The selection is or is in a compound shape.
fn has_compound_shape(s: &Session) -> std::result::Result<(), String> {
let st = s.active().ok_or("no document open")?;
if st.selection.objects.iter().any(|id| shape_of(&st.doc, *id).is_some()) { Ok(()) } else { Err("select a compound shape".into()) }
}
/// The selection is or is in a member of a compound shape.
fn has_member(s: &Session) -> std::result::Result<(), String> {
let st = s.active().ok_or("no document open")?;
if st.selection.objects.iter().any(|id| holder(&st.doc, *id).is_some()) { Ok(()) } else { Err("select members of a compound shape".into()) }
}
/// The `mode` param: one of [`MODE_NAMES`], or `default` when it is missing (`None`: required).
fn mode_param(p: &Value, cmd: &str, default: Option<ShapeMode>) -> Result<ShapeMode> {
match super::object::choice(p, cmd, "mode", &MODE_NAMES)? {
Some(name) => ShapeMode::parse(name),
None => default,
}
.ok_or_else(|| bad(cmd, format!("`mode` must be one of {}", MODE_NAMES.join(", "))))
}
fn ids_json(ids: &[NodeId]) -> Value {
json!({ "ids": ids.iter().map(|i| i.0).collect::<Vec<_>>() })
}
fn make(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "object.compoundShape.make";
let mode = mode_param(p, C, Some(ShapeMode::Add))?;
let roots = selected_roots(s)?;
if roots.len() < 2 {
return Err(bad(C, "select at least two objects"));
}
let d = &s.doc()?.doc;
let mut members = Vec::with_capacity(roots.len());
for id in &roots {
let n = d.node(*id).ok_or(EngineError::NoNode(*id))?;
if !matches!(n.kind, NodeKind::Path { guide: false, .. } | NodeKind::Compound { .. } | NodeKind::Group { .. } | NodeKind::Text(_)) {
return Err(bad(C, "compound shapes take paths, compound paths, groups, compound shapes and type"));
}
members.push(n);
}
// The paint and transparency of the topmost member in Add, Intersect or Exclude mode: as they
// all have `mode`, the topmost, or the backmost when they all subtract.
let order: Vec<&Node> = if mode == ShapeMode::Subtract { members } else { members.into_iter().rev().collect() };
let style = order.into_iter().find_map(|n| {
let mut lv = vec![];
pf_leaves(n, &mut lv);
lv.last().map(style_of)
});
let id = s.edit("Make Compound Shape", |d, sel| {
let gid = d.alloc_id();
let id = d.wrap(&roots, |children| {
let children = children
.into_iter()
.map(|c| {
let mut c = Arc::unwrap_or_clone(c);
c.extras_mut().shape_mode = mode;
Arc::new(c)
})
.collect();
let mut g = Node::group(gid, children);
g.extras_mut().compound_shape = true;
if let Some(style) = &style {
style.apply(&mut g);
}
g
})?;
sel.set([id]);
Ok(id)
})?;
Ok(json!({ "id": id.0 }))
}
fn set_mode(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "object.compoundShape.setMode";
let mode = mode_param(p, C, None)?;
let members = selected_members(s)?;
if members.is_empty() {
return Err(bad(C, "select members of a compound shape"));
}
// Members already in the mode leave the document and its undo history as they are.
let d = &s.doc()?.doc;
if members.iter().all(|id| d.node(*id).is_some_and(|n| n.extras().shape_mode == mode)) {
return Ok(ids_json(&members));
}
s.edit("Shape Mode", |d, _| {
for id in &members {
d.node_mut(*id).ok_or(EngineError::NoNode(*id))?.extras_mut().shape_mode = mode;
}
Ok(())
})?;
Ok(ids_json(&members))
}
fn release(s: &mut Session, _: &Value) -> Result<Value> {
let shapes = selected_shapes(s)?;
if shapes.is_empty() {
return Err(bad("object.compoundShape.release", "select a compound shape"));
}
let ids = s.edit("Release Compound Shape", |d, sel| {
let (mut items, mut released) = (vec![], vec![]);
for id in &shapes {
let members = d.node(*id).and_then(Node::children).ok_or(EngineError::NoNode(*id))?;
let out: Vec<Node> = members
.iter()
.map(|c| {
let mut c = (**c).clone();
c.extras_mut().shape_mode = ShapeMode::Add;
released.push(c.id);
c
})
.collect();
items.push((*id, out));
}
d.insert_beside(items, true)?;
sel.set(released.iter().copied());
Ok(released)
})?;
Ok(ids_json(&ids))
}
fn expand(s: &mut Session, _: &Value) -> Result<Value> {
const LABEL: &str = "Expand Compound Shape";
let shapes = selected_shapes(s)?;
if shapes.is_empty() {
return Err(bad("object.compoundShape.expand", "select a compound shape"));
}
let ids = s.edit(LABEL, |d, sel| {
let (mut items, mut made) = (vec![], vec![]);
for id in &shapes {
let shape = d.node(*id).cloned().ok_or(EngineError::NoNode(*id))?;
let area = effects::compound_shape_area(&shape, effects::text_outliner())
.ok_or_else(|| EngineError::Other("the compound shape is too complex to expand".into()))?;
if area.is_empty() {
return Err(EngineError::Other(format!("{LABEL} produced an empty result")));
}
let mut n = shape_node(d, area, None);
let mut appearance = (*shape.appearance).clone();
appearance.contents_index = None;
n.appearance = appearance.into();
(n.opacity, n.blend, n.name) = (shape.opacity, shape.blend, shape.name.clone());
made.push(n.id);
items.push((*id, vec![n]));
}
d.insert_beside(items, true)?;
sel.set(made.iter().copied());
Ok(made)
})?;
Ok(ids_json(&ids))
}
+18 -4
View File
@@ -226,7 +226,7 @@ fn ids_json(ids: &[NodeId]) -> Value {
/// Paint identity carried through a Pathfinder operation.
#[derive(Clone, Debug, PartialEq)]
struct Style {
pub(crate) struct Style {
appearance: Appearance,
opacity: f32,
blend: BlendMode,
@@ -236,7 +236,7 @@ impl Style {
fn of(n: &Node) -> Self {
Style { appearance: (*n.appearance).clone(), opacity: n.opacity, blend: n.blend }
}
fn apply(&self, n: &mut Node) {
pub(crate) fn apply(&self, n: &mut Node) {
n.appearance = self.appearance.clone().into();
n.opacity = self.opacity;
n.blend = self.blend;
@@ -267,7 +267,7 @@ pub(crate) fn text_outline(t: &TextObject) -> PathData {
PathData::from_bezpath(&lay.to_bezpath()).transformed(t.xf)
}
fn style_of(n: &Node) -> Style {
pub(crate) fn style_of(n: &Node) -> Style {
match &n.kind {
NodeKind::Text(t) => Style { appearance: t.first_style().appearance(), opacity: n.opacity, blend: n.blend },
_ => Style::of(n),
@@ -294,6 +294,20 @@ fn each_leaf(n: &Node, f: &mut impl FnMut(&Node)) {
}
}
/// [`leaves`] as a Pathfinder operation takes them: a compound shape is one shape, the area it
/// draws painted as it is (its members as they are past a live Pathfinder's budget).
pub(crate) fn pf_leaves(n: &Node, out: &mut Vec<Node>) {
match &n.kind {
NodeKind::Group { children, .. } | NodeKind::Layer { children, .. } => {
match vectorcraft_render::effects::compound_shape_art(n, vectorcraft_render::effects::text_outliner()) {
Some(art) => out.push(art),
None => children.iter().for_each(|c| pf_leaves(c, out)),
}
}
_ => leaves(n, out),
}
}
/// How many shapes a Pathfinder operation takes from `n` selected alone: its leaf shapes, however
/// deep its groups nest (an expanded stroke is a group inside the group, #1635).
pub fn shape_count(n: &Node) -> usize {
@@ -554,7 +568,7 @@ fn run_pf(s: &mut Session, p: &Value, op: PathfinderOp) -> Result<Value> {
for id in &roots {
let n = d.node(*id).ok_or(EngineError::NoNode(*id))?;
let mut lv = vec![];
leaves(n, &mut lv);
pf_leaves(n, &mut lv);
if lv.is_empty() {
continue;
}
+4
View File
@@ -77,6 +77,10 @@ pub(crate) const SELECTS_GIVEN_IDS: &[&str] = &[
"object.clippingMask.release",
"object.compoundPath.make",
"object.compoundPath.release",
"object.compoundShape.expand",
"object.compoundShape.make",
"object.compoundShape.release",
"object.compoundShape.setMode",
"object.group",
"object.hide",
"object.lock",
+1
View File
@@ -30,6 +30,7 @@ mod colorize;
pub(super) mod colormgmt;
mod colorthemes;
mod colourfonts;
mod compound_shape;
mod compoundpaint;
mod constrain_new;
mod constrain_tools;
+196
View File
@@ -0,0 +1,196 @@
//! Pathfinder panel › compound shapes (`object.compoundShape.*`): Make Compound Shape in each
//! shape mode, a member's mode, Release, Expand, Undo, saving and exporting.
use serde_json::{Value, json};
use vectorcraft_color::Color;
use vectorcraft_doc::{Node, ShapeMode};
use vectorcraft_render::effects;
use crate::*;
fn run(s: &mut Session, id: &str, p: Value) -> Value {
s.execute(id, &p).unwrap()
}
fn node(s: &Session, id: NodeId) -> Node {
s.doc().unwrap().doc.node(id).cloned().unwrap()
}
fn area(path: &vectorcraft_geom::PathData) -> f64 {
vectorcraft_pathops::area(path, vectorcraft_geom::FillRule::NonZero).abs()
}
/// The area a path or compound path fills.
fn node_area(n: &Node) -> f64 {
area(&crate::cmd::pathops::node_path(n).unwrap().0)
}
fn square(s: &mut Session, x: f64, color: &str) -> NodeId {
let id = NodeId(run(s, "shape.rectangle", json!({"x": x, "y": 0, "width": 100, "height": 100}))["id"].as_u64().unwrap());
run(s, "paint.setFill", json!({"color": color}));
id
}
/// A red square (0–100) behind a blue one (50–150), both 100 high and selected → (session, back, front).
fn two_squares() -> (Session, NodeId, NodeId) {
let mut s = Session::new();
run(&mut s, "file.new", json!({"width": 400, "height": 300}));
let back = square(&mut s, 0.0, "#ff0000");
let front = square(&mut s, 50.0, "#0000ff");
run(&mut s, "select.set", json!({"ids": [back.0, front.0]}));
(s, back, front)
}
fn selected(s: &Session) -> Vec<NodeId> {
s.doc().unwrap().selection.objects.clone()
}
/// Make Compound Shape (Alt-clicking a Shape Mode gives its mode) keeps both squares as members in
/// the mode, takes the paint of the topmost member not subtracting, draws the area the modes make,
/// and Expand turns it into that area as one path; Undo brings each step back.
#[test]
fn make_and_expand_each_shape_mode() {
for (mode, want, fill, kind) in [
("add", 15_000.0, Color::rgb(0.0, 0.0, 1.0), "Path"),
("subtract", 5_000.0, Color::rgb(1.0, 0.0, 0.0), "Path"),
("intersect", 5_000.0, Color::rgb(0.0, 0.0, 1.0), "Path"),
("exclude", 10_000.0, Color::rgb(0.0, 0.0, 1.0), "Compound Path"),
] {
let (mut s, back, front) = two_squares();
let before = s.doc().unwrap().doc.clone();
let id = NodeId(run(&mut s, "object.compoundShape.make", json!({"mode": mode}))["id"].as_u64().unwrap());
assert_eq!(selected(&s), vec![id]);
let shape = node(&s, id);
assert!(shape.is_compound_shape(), "{mode}");
assert_eq!(shape.kind_label(), "Compound Shape");
let members: Vec<NodeId> = shape.children().unwrap().iter().map(|m| m.id).collect();
assert_eq!(members, vec![back, front], "the members, in their stacking order");
assert!(shape.children().unwrap().iter().all(|m| m.extras().shape_mode.name() == mode));
assert_eq!(shape.appearance.fill_paint().color(), Some(fill), "{mode}");
let drawn = effects::compound_shape_area(&shape, None).unwrap();
assert!((area(&drawn) - want).abs() < 0.5, "{mode}: {}", area(&drawn));
// Expand: the path, or compound path, of the area, painted as the compound shape.
let made = run(&mut s, "object.compoundShape.expand", json!({}));
let path = NodeId(made["ids"][0].as_u64().unwrap());
let n = node(&s, path);
assert_eq!(n.kind_label(), kind, "{mode}");
assert!((node_area(&n) - want).abs() < 0.5, "{mode}");
assert_eq!(n.appearance.fill_paint().color(), Some(fill));
assert_eq!(selected(&s), vec![path]);
assert!(s.doc().unwrap().doc.node(back).is_none() && s.doc().unwrap().doc.node(id).is_none());
run(&mut s, "edit.undo", json!({}));
assert!(node(&s, id).is_compound_shape());
run(&mut s, "edit.undo", json!({}));
assert_eq!(s.doc().unwrap().doc.layers, before.layers, "{mode}: Undo leaves the squares as they were");
}
}
/// Make Compound Shape from the panel menu gives every member Add; Release gives the members back
/// as they were, selected, in one undo step.
#[test]
fn release_returns_the_members_as_they_were() {
let (mut s, back, front) = two_squares();
let before = s.doc().unwrap().doc.clone();
let id = NodeId(run(&mut s, "object.compoundShape.make", json!({}))["id"].as_u64().unwrap());
assert!(node(&s, id).children().unwrap().iter().all(|m| m.extras().shape_mode == ShapeMode::Add));
run(&mut s, "object.compoundShape.setMode", json!({"mode": "subtract", "ids": [front.0]}));
run(&mut s, "select.set", json!({"ids": [id.0]}));
let undo = s.doc().unwrap().history.undo.len();
let out = run(&mut s, "object.compoundShape.release", json!({}));
assert_eq!(out["ids"], json!([back.0, front.0]));
assert_eq!(selected(&s), vec![back, front]);
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1);
assert_eq!(s.doc().unwrap().doc.layers, before.layers, "the members' paint and place, no shape modes");
// Nothing selected is in a compound shape: Release and Expand are off.
assert!(s.execute("object.compoundShape.release", &json!({})).is_err());
assert!(s.execute("object.compoundShape.expand", &json!({})).is_err());
assert!(s.execute("object.compoundShape.setMode", &json!({"mode": "add"})).is_err());
}
/// A member selected (Direct Selection) takes the mode of the Shape Mode clicked; the area follows,
/// and Undo puts the mode back. Release and Expand act on the compound shape it is in.
#[test]
fn a_selected_member_takes_a_shape_mode() {
let (mut s, _, front) = two_squares();
let id = NodeId(run(&mut s, "object.compoundShape.make", json!({}))["id"].as_u64().unwrap());
run(&mut s, "select.set", json!({"ids": [front.0]}));
assert_eq!(cmd::selected_member_modes(&s), vec![ShapeMode::Add]);
run(&mut s, "object.compoundShape.setMode", json!({"mode": "intersect"}));
assert_eq!(cmd::selected_member_modes(&s), vec![ShapeMode::Intersect]);
assert!((area(&effects::compound_shape_area(&node(&s, id), None).unwrap()) - 5_000.0).abs() < 0.5);
// The same mode again is no edit.
let undo = s.doc().unwrap().history.undo.len();
run(&mut s, "object.compoundShape.setMode", json!({"mode": "intersect"}));
assert_eq!(s.doc().unwrap().history.undo.len(), undo);
assert!(s.execute("object.compoundShape.setMode", &json!({"mode": "divide"})).is_err());
run(&mut s, "edit.undo", json!({}));
assert_eq!(node(&s, front).extras().shape_mode, ShapeMode::Add);
run(&mut s, "select.set", json!({"ids": [front.0]}));
let made = run(&mut s, "object.compoundShape.expand", json!({}));
assert!((node_area(&node(&s, NodeId(made["ids"][0].as_u64().unwrap()))) - 15_000.0).abs() < 0.5);
}
/// A compound shape holds its paint: a fill given to it (or to a member) paints the compound shape,
/// whose members keep theirs.
#[test]
fn paint_goes_to_the_compound_shape() {
let (mut s, back, front) = two_squares();
let id = NodeId(run(&mut s, "object.compoundShape.make", json!({}))["id"].as_u64().unwrap());
let out = run(&mut s, "paint.setFill", json!({"color": "#00ff00"}));
assert_eq!(out["objects"], 1);
assert_eq!(node(&s, id).appearance.fill_paint().color(), Some(Color::rgb(0.0, 1.0, 0.0)));
run(&mut s, "paint.setFill", json!({"color": "#ffff00", "ids": [front.0]}));
assert_eq!(node(&s, id).appearance.fill_paint().color(), Some(Color::rgb(1.0, 1.0, 0.0)));
assert_eq!(node(&s, back).appearance.fill_paint().color(), Some(Color::rgb(1.0, 0.0, 0.0)));
assert_eq!(node(&s, front).appearance.fill_paint().color(), Some(Color::rgb(0.0, 0.0, 1.0)));
}
/// Make Compound Shape needs two or more objects it can take.
#[test]
fn make_needs_two_objects_it_can_take() {
let (mut s, back, _) = two_squares();
run(&mut s, "select.set", json!({"ids": [back.0]}));
assert!(s.execute("object.compoundShape.make", &json!({})).is_err());
let (mut s, back, front) = two_squares();
assert!(s.execute("object.compoundShape.make", &json!({"mode": "divide"})).is_err());
assert_eq!(selected(&s), vec![back, front], "nothing changed");
}
/// Saved and opened again, a compound shape keeps its members and their modes; a mode a file names
/// that isn't one reads as Add.
#[test]
fn compound_shapes_survive_a_save() {
let (mut s, _, front) = two_squares();
let id = NodeId(run(&mut s, "object.compoundShape.make", json!({"mode": "subtract"}))["id"].as_u64().unwrap());
let doc = s.doc().unwrap().doc.clone();
let bytes = vectorcraft_format::save(&doc, false);
let back = vectorcraft_format::load(&bytes).unwrap();
assert_eq!(back.node(id), doc.node(id));
assert!(back.node(id).unwrap().is_compound_shape());
assert_eq!(back.node(front).unwrap().extras().shape_mode, ShapeMode::Subtract);
let text = String::from_utf8(bytes).unwrap().replace("\"shape_mode\":\"subtract\"", "\"shape_mode\":\"divide\"");
let odd = vectorcraft_format::load(text.as_bytes()).unwrap();
assert_eq!(odd.node(front).unwrap().extras().shape_mode, ShapeMode::Add);
}
/// SVG export draws the area, painted as the compound shape, not the members.
#[test]
fn svg_export_draws_the_area() {
let (mut s, _, _) = two_squares();
run(&mut s, "object.compoundShape.make", json!({"mode": "subtract"}));
let svg = vectorcraft_svg::export(&s.doc().unwrap().doc, &Default::default());
assert!(svg.contains("#ff0000"), "the back square's red paints the area");
assert!(!svg.contains("#0000ff"), "the front square isn't drawn: {svg}");
}
/// The Shape Modes and Pathfinders take a compound shape as the area it draws.
#[test]
fn pathfinder_operations_take_the_area() {
let (mut s, _, _) = two_squares();
let id = NodeId(run(&mut s, "object.compoundShape.make", json!({"mode": "subtract"}))["id"].as_u64().unwrap());
let r = NodeId(run(&mut s, "shape.rectangle", json!({"x": 200, "y": 0, "width": 50, "height": 100}))["id"].as_u64().unwrap());
run(&mut s, "select.set", json!({"ids": [id.0, r.0]}));
let out = run(&mut s, "object.pathfinder.unite", json!({}));
let n = node(&s, NodeId(out["ids"][0].as_u64().unwrap()));
assert!((node_area(&n) - 10_000.0).abs() < 0.5, "the 50 × 100 area and the 50 × 100 rectangle: {}", node_area(&n));
}
@@ -73,6 +73,17 @@ fn ids_are_selected_first() {
s.execute("select.none", &json!({})).unwrap();
vec![c]
};
let shape = |s: &mut Session, ids: &[u64]| {
s.execute("select.set", &json!({"ids": &ids[..2]})).unwrap();
let c = s.execute("object.compoundShape.make", &json!({})).unwrap()["id"].as_u64().unwrap();
s.execute("select.none", &json!({})).unwrap();
c
};
let shaped = |s: &mut Session, ids: &[u64]| vec![shape(s, ids)];
let member = |s: &mut Session, ids: &[u64]| {
shape(s, ids);
vec![ids[1]]
};
let one = |_: &mut Session, ids: &[u64]| vec![ids[1]];
let cases: Vec<(&str, Value, Setup<'_>)> = vec![
("object.group", json!({}), &none),
@@ -81,6 +92,10 @@ fn ids_are_selected_first() {
("object.clippingMask.release", json!({}), &clipped),
("object.compoundPath.make", json!({}), &none),
("object.compoundPath.release", json!({}), &compound),
("object.compoundShape.make", json!({"mode": "intersect"}), &none),
("object.compoundShape.setMode", json!({"mode": "subtract"}), &member),
("object.compoundShape.release", json!({}), &shaped),
("object.compoundShape.expand", json!({}), &shaped),
("object.arrange.bringToFront", json!({}), &one),
("object.arrange.bringForward", json!({}), &one),
("object.arrange.sendBackward", json!({}), &one),
+12
View File
@@ -141,6 +141,8 @@ pub enum Op {
ClipRelease,
Compound,
CompoundRelease,
/// Make Compound Shape in one of the four shape modes, a member's mode, Release or Expand.
CompoundShape(u8),
Lock,
UnlockAll,
Hide,
@@ -238,6 +240,7 @@ fn arb_leaf_op() -> impl Strategy<Value = Op> {
1 => Just(Op::ClipRelease),
1 => Just(Op::Compound),
1 => Just(Op::CompoundRelease),
1 => (0u8..7).prop_map(Op::CompoundShape),
1 => Just(Op::Lock),
1 => Just(Op::UnlockAll),
1 => Just(Op::Hide),
@@ -369,6 +372,15 @@ impl Op {
Op::ClipRelease => c("object.clippingMask.release", json!({})),
Op::Compound => c("object.compoundPath.make", json!({})),
Op::CompoundRelease => c("object.compoundPath.release", json!({})),
Op::CompoundShape(i) => {
const MODES: [&str; 4] = ["add", "subtract", "intersect", "exclude"];
match i % 7 {
i @ 0..4 => c("object.compoundShape.make", json!({"mode": MODES[i as usize]})),
4 => c("object.compoundShape.setMode", json!({"mode": "exclude"})),
5 => c("object.compoundShape.release", json!({})),
_ => c("object.compoundShape.expand", json!({})),
}
}
Op::Lock => c("object.lock", json!({})),
Op::UnlockAll => c("object.unlockAll", json!({})),
Op::Hide => c("object.hide", json!({})),
+2 -1
View File
@@ -550,6 +550,7 @@ font-technology Variable Променливи
Complexity: Сложност:
Composite colours are converted to it; separations separate with it when it is a CMYK profile. Композитните цветове се конвертират в него; сепарациите се сепарират с него, когато е CMYK профил.
Compound Path Съставен път
Compound Shape Съставна фигура
Compress (RLE) Компресиране (RLE)
Compress text and line art Компресиране на текст и линеарна графика
Compression Компресия
@@ -974,7 +975,6 @@ font-technology Variable Променливи
Exit Isolation Mode Излизане от режим на изолация
Expand Разширяване
Expand Appearance Разширяване на външния вид
Expand applies to compound shapes (Alt-click a shape mode) — on the roadmap Разширяването се прилага към съставни фигури (Alt-клик върху режим на фигура) — в план-графика
Expand Brush Strokes Разширяване на щрихи на четка
Expand Compound Shape Разширяване на съставна фигура
Expand Gradient Mesh Разширяване на градиентна мрежа
@@ -2904,6 +2904,7 @@ font-technology Variable Променливи
Shape Builder Regions Области на създателя на фигури
Shape Builder Tool Инструмент „Създател на фигури“
Shape Effects Ефекти за фигури
Shape Mode Режим на фигура
Shape Modes: Режими на фигури:
Shape Properties: Свойства на фигурата:
Shaper Tool Инструмент „Shaper“
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2 -1
View File
@@ -789,6 +789,7 @@
Compound Compost
Compound 2 Compost 2
Compound Path Traçat compost
Compound Shape Forma composta
Compress (RLE) Comprimeix (RLE)
Compress text and line art Comprimeix text i dibuix de línies
Compression Compressió
@@ -1399,7 +1400,6 @@
Expand Expandeix
Expand 1 Selected Expandeix 1 seleccionat
Expand Appearance Expandeix l’aparença
Expand applies to compound shapes (Alt-click a shape mode) — on the roadmap Expandeix s’aplica a les formes compostes (fes Alt-clic en un mode de forma) — previst a la llista de funcions
Expand Blend Expandeix la fusió
Expand Brush Strokes Expandeix els traços del pinzell
Expand Compound Shape Expandeix la forma composta
@@ -3735,6 +3735,7 @@
Shape Builder Regions Regions del constructor de formes
Shape Builder Tool Eina Constructor de formes
Shape Effects Efectes de forma
Shape Mode Mode de forma
Shape Modes: Modes de forma:
Shape Properties: Propietats de la forma:
Shape: Forma:
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2 -1
View File
@@ -287,6 +287,7 @@
Clipping Mask Ořezová maska
Edit Clipping Path Upravit ořezovou cestu
Compound Path Složená cesta
Compound Shape Složený tvar
Artboards Pracovní plochy
Convert to Artboards Převést na pracovní plochy
Rearrange All Artboards… Přeuspořádat všechny pracovní plochy…
@@ -1832,7 +1833,6 @@ lighting Light Světlo
Bottom-to-Bottom Leading Proklad od dolní k dolní
Burasagari Burasagari
Force Vynutit
Expand applies to compound shapes (Alt-click a shape mode) — on the roadmap Rozložení se týká složených tvarů (Alt+kliknutí na režim tvaru) — v plánu
Expand Compound Shape Rozložit složený tvar
Make Compound Shape Vytvořit složený tvar
Pathfinder Options… Možnosti operací s cestami…
@@ -3990,6 +3990,7 @@ font-technology Variable Variabilní
Default Fill and Stroke (D) Výchozí výplň a tah (D)
Fill (X), double-click for the Color Picker Výplň (X), dvojklikem otevřete výběr barvy
Opacity: Krytí:
Shape Mode Režim tvaru
Shape Modes: Režimy tvarů:
Stroke (X), double-click for the Color Picker Tah (X), dvojklikem otevřete výběr barvy
Stroke: Tah:
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2 -1
View File
@@ -259,6 +259,7 @@
Clipping Mask Beskæringsmask
Edit Clipping Path Redigér beskæringskurve
Compound Path Sammensat kurve
Compound Shape Sammensat figur
Artboards Tegneflader
Convert to Artboards Konvertér til tegneflader
Rearrange All Artboards… Omarrangér alle tegneflader…
@@ -1644,7 +1645,6 @@ lighting Light Lys
Bottom-to-Bottom Leading Linjeafstand bund til bund
Burasagari Burasagari
Force Tving
Expand applies to compound shapes (Alt-click a shape mode) — on the roadmap Udvid gælder sammensatte figurer (Alt-klik på en figurtilstand) — på vejkortet
Expand Compound Shape Udvid sammensat figur
Make Compound Shape Opret sammensat figur
Pathfinder Options… Pathfinder-indstillinger…
@@ -3725,6 +3725,7 @@ font-technology Variable Variable
Default Fill and Stroke (D) Standardfyld og -kontur (D)
Fill (X), double-click for the Color Picker Fyld (X), dobbeltklik for at åbne farvevælgeren
Opacity: Opacitet:
Shape Mode Formtilstand
Shape Modes: Formtilstande:
Stroke (X), double-click for the Color Picker Kontur (X), dobbeltklik for at åbne farvevælgeren
Stroke: Kontur:
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2 -1
View File
@@ -543,6 +543,7 @@ font-technology Variable Variabel
Complexity: Komplexität:
Composite colours are converted to it; separations separate with it when it is a CMYK profile. Composite-Farben werden in dieses Profil konvertiert; Separationen werden damit erzeugt, wenn es ein CMYK-Profil ist.
Compound Path Zusammengesetzter Pfad
Compound Shape Zusammengesetzte Form
Compress (RLE) Komprimieren (RLE)
Compress text and line art Text und Strichgrafiken komprimieren
Compression Komprimierung
@@ -968,7 +969,6 @@ font-technology Variable Variabel
Exit Isolation Mode Isolationsmodus beenden
Expand Umwandeln
Expand Appearance Aussehen umwandeln
Expand applies to compound shapes (Alt-click a shape mode) — on the roadmap „Umwandeln“ gilt für zusammengesetzte Formen (Alt-Klick auf einen Formmodus) – geplant
Expand Brush Strokes Pinselkonturen umwandeln
Expand Compound Shape Zusammengesetzte Form umwandeln
Expand Gradient Mesh Verlaufsgitter umwandeln
@@ -2939,6 +2939,7 @@ mark-style Japanese Japanisch
Shape Builder Regions Formerstellungsbereiche
Shape Builder Tool Formerstellungs-Werkzeug
Shape Effects Formeffekte
Shape Mode Formmodus
Shape Modes: Formmodi:
Shape Properties: Formeigenschaften:
Shaper Tool Shaper-Werkzeug
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2 -1
View File
@@ -524,6 +524,7 @@ font-technology Variable Μεταβλητές
Complement Συμπληρωματικό
Complexity: Πολυπλοκότητα:
Compound Path Σύνθετη διαδρομή
Compound Shape Σύνθετο σχήμα
Compress (RLE) Συμπίεση (RLE)
Compress text and line art Συμπίεση κειμένου και γραμμικών γραφικών
Compression Συμπίεση
@@ -2500,7 +2501,6 @@ theme Appearance Εμφάνιση
Each page prints as one image, at the document's raster effects resolution. Κάθε σελίδα εκτυπώνεται ως μία εικόνα, στην ανάλυση εφέ ράστερ του εγγράφου.
English: UK Αγγλικά: Ηνωμένο Βασίλειο
English: USA Αγγλικά: ΗΠΑ
Expand applies to compound shapes (Alt-click a shape mode) — on the roadmap Η ανάπτυξη εφαρμόζεται σε σύνθετα σχήματα (Alt-κλικ σε λειτουργία σχήματος) — προγραμματισμένη λειτουργία
French Γαλλικά
Generate CSS: the whole document's CSS while nothing is selected Δημιουργία CSS: το CSS ολόκληρου του εγγράφου όταν δεν υπάρχει επιλογή
German Γερμανικά
@@ -3409,6 +3409,7 @@ theme Appearance Εμφάνιση
Shadows and Glows Σκιές και λάμψεις
Shape Builder Regions Περιοχές δημιουργίας σχημάτων
Shape Effects Εφέ σχημάτων
Shape Mode Λειτουργία σχήματος
Shape Modes: Λειτουργίες σχημάτων:
Shape Properties: Ιδιότητες σχήματος:
Shaper Tool Διαμορφωτής σχημάτων (Shaper Tool)
Can't render this file because it contains an unexpected character in line 164 and column 5.
+2 -1
View File
@@ -565,6 +565,7 @@ font-technology Variable Variables
Complexity: Complejidad:
Composite colours are converted to it; separations separate with it when it is a CMYK profile. Los colores compuestos se convierten a este perfil; las separaciones lo usan cuando es un perfil CMYK.
Compound Path Trazado compuesto
Compound Shape Forma compuesta
Compress (RLE) Comprimir (RLE)
Compress text and line art Comprimir texto y arte lineal
Compression Compresión
@@ -991,7 +992,6 @@ font-technology Variable Variables
Exit Isolation Mode Salir del modo de aislamiento
Expand Expandir
Expand Appearance Expandir apariencia
Expand applies to compound shapes (Alt-click a shape mode) — on the roadmap Expandir se aplica a formas compuestas (Alt+clic en un modo de forma): en la hoja de ruta
Expand Brush Strokes Expandir trazos de pincel
Expand Compound Shape Expandir forma compuesta
Expand Gradient Mesh Expandir malla de degradado
@@ -2974,6 +2974,7 @@ font-technology Variable Variables
Shape Builder Regions Regiones del generador de formas
Shape Builder Tool Herramienta Generador de formas
Shape Effects Efectos de forma
Shape Mode Modo de forma
Shape Modes: Modos de forma:
Shape Properties: Propiedades de forma:
Shaper Tool Herramienta Shaper
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2 -1
View File
@@ -294,6 +294,7 @@
Clipping Mask Leikkausmaski
Edit Clipping Path Muokkaa leikkauspolkua
Compound Path Yhdistelmäpolku
Compound Shape Yhdistelmämuoto
Artboards Piirtoalustat
Convert to Artboards Muunna piirtoalustoiksi
Rearrange All Artboards… Järjestä kaikki piirtoalustat uudelleen…
@@ -1794,7 +1795,6 @@ lighting Light Valo
Bottom-to-Bottom Leading Riviväli alareunasta alareunaan
Burasagari Burasagari
Force Pakota
Expand applies to compound shapes (Alt-click a shape mode) — on the roadmap Laajenna koskee yhdistelmämuotoja (Alt-napsauta muototilaa) — suunnitteilla
Expand Compound Shape Laajenna yhdistelmämuoto
Make Compound Shape Tee yhdistelmämuoto
Pathfinder Options… Polunetsimen asetukset…
@@ -3910,6 +3910,7 @@ font-technology Variable Muuttuvat
Default Fill and Stroke (D) Oletustäyttö ja -viiva (D)
Fill (X), double-click for the Color Picker Täyttö (X), kaksoisnapsauta avataksesi värinvalitsimen
Opacity: Peittävyys:
Shape Mode Muototila
Shape Modes: Muototilat:
Stroke (X), double-click for the Color Picker Viiva (X), kaksoisnapsauta avataksesi värinvalitsimen
Stroke: Viiva:
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2 -1
View File
@@ -566,6 +566,7 @@ font-technology Variable Variables
Complexity: Complexité :
Composite colours are converted to it; separations separate with it when it is a CMYK profile. Les couleurs composites sont converties vers ce profil ; les séparations l’utilisent lorsqu’il s’agit d’un profil CMJN.
Compound Path Tracé transparent
Compound Shape Forme composée
Compress (RLE) Compresser (RLE)
Compress text and line art Compresser le texte et les dessins au trait
Compression Compression
@@ -991,7 +992,6 @@ font-technology Variable Variables
Exit Isolation Mode Quitter le mode Isolation
Expand Décomposer
Expand Appearance Décomposer l’aspect
Expand applies to compound shapes (Alt-click a shape mode) — on the roadmap Décomposer s’applique aux formes composées (Alt+clic sur un mode de forme) — prévu dans la feuille de route
Expand Brush Strokes Décomposer les contours de forme
Expand Compound Shape Décomposer la forme composée
Expand Gradient Mesh Décomposer le filet de dégradé
@@ -2971,6 +2971,7 @@ font-technology Variable Variables
Shape Builder Regions Zones du Concepteur de forme
Shape Builder Tool Outil Concepteur de forme
Shape Effects Effets de forme
Shape Mode Mode de forme
Shape Modes: Modes de forme :
Shape Properties: Propriétés de la forme :
Shaper Tool Outil Shaper
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2 -1
View File
@@ -749,6 +749,7 @@ font-technology Variable Változó
Compound Összetett
Compound 2 Összetett 2
Compound Path Összetett útvonal
Compound Shape Összetett alakzat
Compress (RLE) Tömörítés (RLE)
Compress text and line art Szöveg és vonalas grafika tömörítése
Compression Tömörítés
@@ -1269,7 +1270,6 @@ font-technology Variable Változó
Expand Kibontás
Expand 1 Selected 1 kijelölt kibontása
Expand Appearance Megjelenés kibontása
Expand applies to compound shapes (Alt-click a shape mode) — on the roadmap A Kibontás összetett alakzatokra vonatkozik (Alt+kattintás egy alakzatmódra) – tervezett funkció
Expand Brush Strokes Ecsetvonások kibontása
Expand Compound Shape Összetett alakzat kibontása
Expand Gradient Mesh Színátmenetes háló kibontása
@@ -3584,6 +3584,7 @@ mark-style Japanese Japán
Shape Builder Regions Alakzatépítő területei
Shape Builder Tool Alakzatépítő
Shape Effects Alakzateffektusok
Shape Mode Alakzatmód
Shape Modes: Alakzatmódok:
Shape Properties: Alakzat tulajdonságai:
Shape: Alak:
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2 -1
View File
@@ -259,6 +259,7 @@
Clipping Mask Masker kliping
Edit Clipping Path Edit jalur kliping
Compound Path Jalur majemuk
Compound Shape Bentuk majemuk
Artboards Artboard
Convert to Artboards Konversi ke artboard
Rearrange All Artboards… Atur ulang semua artboard…
@@ -1644,7 +1645,6 @@ lighting Light Cahaya
Bottom-to-Bottom Leading Leading dari bawah ke bawah
Burasagari Burasagari
Force Paksa
Expand applies to compound shapes (Alt-click a shape mode) — on the roadmap Perluas berlaku untuk bentuk majemuk (Alt+klik mode bentuk) — masih dalam roadmap
Expand Compound Shape Perluas bentuk majemuk
Make Compound Shape Buat bentuk majemuk
Pathfinder Options… Opsi Pathfinder…
@@ -3725,6 +3725,7 @@ font-technology Variable Variabel
Default Fill and Stroke (D) Isi dan goresan default (D)
Fill (X), double-click for the Color Picker Isi (X), klik ganda untuk pemilih warna
Opacity: Opasitas:
Shape Mode Mode bentuk
Shape Modes: Mode bentuk:
Stroke (X), double-click for the Color Picker Goresan (X), klik ganda untuk pemilih warna
Stroke: Goresan:
Can't render this file because it contains an unexpected character in line 2169 and column 5.
+2 -1
View File
@@ -565,6 +565,7 @@ font-technology Variable Variabili
Complexity: Complessità:
Composite colours are converted to it; separations separate with it when it is a CMYK profile. I colori compositi vengono convertiti in questo profilo; le separazioni lo usano quando è un profilo CMYK.
Compound Path Tracciato composto
Compound Shape Forma composta
Compress (RLE) Comprimi (RLE)
Compress text and line art Comprimi testo e grafica al tratto
Compression Compressione
@@ -991,7 +992,6 @@ font-technology Variable Variabili
Exit Isolation Mode Esci dalla modalità isolamento
Expand Espandi
Expand Appearance Espandi aspetto
Expand applies to compound shapes (Alt-click a shape mode) — on the roadmap Espandi si applica alle forme composte (Alt+clic su un metodo forma) — in programma
Expand Brush Strokes Espandi tracce pennello
Expand Compound Shape Espandi forma composta
Expand Gradient Mesh Espandi trama sfumatura
@@ -3008,6 +3008,7 @@ mark-style Japanese Giapponese
Shape Builder Regions Aree del Generatore forme
Shape Builder Tool Strumento Generatore forme
Shape Effects Effetti forma
Shape Mode Metodo forma
Shape Modes: Metodi forma:
Shape Properties: Proprietà forma:
Shaper Tool Strumento Sagomatore
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2 -1
View File
@@ -294,6 +294,7 @@ font-technology Variable バリアブル
Clipping Mask クリッピングマスク
Edit Clipping Path クリッピングパスを編集
Compound Path 複合パス
Compound Shape 複合シェイプ
Artboards アートボード
Convert to Artboards アートボードに変換
Rearrange All Artboards… すべてのアートボードを再配置…
@@ -1463,7 +1464,6 @@ font-technology Variable バリアブル
Expand Layers Panel Rows レイヤーパネルの行を展開
Expand Options 拡張オプション
Expand Repeat リピートを拡張
Expand applies to compound shapes (Alt-click a shape mode) — on the roadmap 拡張は複合シェイプ (Alt キーを押しながらシェイプモードをクリック)に適用されます。今後対応予定です。
Expand: 拡張:
Export All… すべて書き出し…
Export Artboard アートボードを書き出し
@@ -2846,6 +2846,7 @@ mark-style Japanese 日本式
Shape Builder シェイプ形成ツール
Shape Builder Regions シェイプ形成ツールの領域
Shape Builder Tool シェイプ形成ツール
Shape Mode シェイプモード
Shape Modes: シェイプモード:
Shape Properties: シェイプのプロパティ:
Shaper Tool Shaper ツール
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2 -1
View File
@@ -220,6 +220,7 @@
Clipping Mask მომჭრელი ნიღაბი
Edit Clipping Path მომჭრელი ტრაექტორიის რედაქტირება
Compound Path შედგენილი ტრაექტორია
Compound Shape შედგენილი ფიგურა
Artboards სამუშაო დაფები
Convert to Artboards სამუშაო დაფებად გარდაქმნა
Rearrange All Artboards… ყველა სამუშაო დაფის გადალაგება…
@@ -1547,7 +1548,6 @@ lighting Light სინათლე
Bottom-to-Bottom Leading სტრიქონთა დაშორება ქვედა კიდიდან ქვედა კიდემდე
Burasagari Burasagari
Force იძულებით
Expand applies to compound shapes (Alt-click a shape mode) — on the roadmap გაშლა შედგენილ ფიგურებზე მოქმედებს (Alt-დაწკაპუნება ფიგურის რეჟიმზე) — სამომავლო გეგმაშია
Expand Compound Shape შედგენილი ფიგურის გაშლა
Make Compound Shape შედგენილი ფიგურის შექმნა
Pathfinder Options… ტრაექტორიების ოპერაციების პარამეტრები…
@@ -3507,6 +3507,7 @@ font-technology Variable ცვლადი
Default Fill and Stroke (D) ნაგულისხმევი შევსება და კონტური (D)
Fill (X), double-click for the Color Picker შევსება (X), ორმაგი დაწკაპუნება — ფერის ამრჩევი
Opacity: გაუმჭვირვალობა:
Shape Mode ფიგურის რეჟიმი
Shape Modes: ფიგურის რეჟიმები:
Stroke (X), double-click for the Color Picker კონტური (X), ორმაგი დაწკაპუნება — ფერის ამრჩევი
Stroke: კონტური:
Can't render this file because it is too large.
+2 -1
View File
@@ -290,6 +290,7 @@ font-technology Variable 가변
Clipping Mask 클리핑 마스크
Edit Clipping Path 클리핑 패스 편집
Compound Path 컴파운드 패스
Compound Shape 컴파운드 모양
Artboards 아트보드
Convert to Artboards 아트보드로 변환
Rearrange All Artboards… 모든 아트보드 재정렬…
@@ -1458,7 +1459,6 @@ font-technology Variable 가변
Expand Layers Panel Rows 레이어 패널 행 펼치기
Expand Options 옵션 펼치기
Expand Repeat 반복 확장
Expand applies to compound shapes (Alt-click a shape mode) — on the roadmap 확장은 컴파운드 모양(모양 모드를 Alt-클릭)에 적용됩니다 — 개발 예정
Expand: 확장:
Export All… 모두 내보내기…
Export Artboard 아트보드 내보내기
@@ -2840,6 +2840,7 @@ mark-style Japanese 일본식
Shape Builder 모양 빌더
Shape Builder Regions 모양 빌더 영역
Shape Builder Tool 모양 빌더 도구
Shape Mode 모양 모드
Shape Modes: 모양 모드:
Shape Properties: 모양 속성:
Shaper Tool 셰이퍼 도구
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2 -1
View File
@@ -273,6 +273,7 @@
Clipping Mask Knipmasker
Edit Clipping Path Knippad bewerken
Compound Path Samengesteld pad
Compound Shape Samengestelde vorm
Artboards Tekengebieden
Convert to Artboards Converteren naar tekengebieden
Rearrange All Artboards… Alle tekengebieden herschikken…
@@ -1658,7 +1659,6 @@ lighting Light Licht
Bottom-to-Bottom Leading Regelafstand van onder naar onder
Burasagari Burasagari
Force Forceren
Expand applies to compound shapes (Alt-click a shape mode) — on the roadmap Uitvouwen is van toepassing op samengestelde vormen (Alt-klik op een vormmodus) — staat op de roadmap
Expand Compound Shape Samengestelde vorm uitvouwen
Make Compound Shape Samengestelde vorm maken
Pathfinder Options… Pathfinder-opties…
@@ -3739,6 +3739,7 @@ font-technology Variable Variabel
Default Fill and Stroke (D) Standaard vulling en lijn (D)
Fill (X), double-click for the Color Picker Vulling (X), dubbelklik voor de kleurenkiezer
Opacity: Dekking:
Shape Mode Vormmodus
Shape Modes: Vormmodi:
Stroke (X), double-click for the Color Picker Lijn (X), dubbelklik voor de kleurenkiezer
Stroke: Lijn:
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2 -1
View File
@@ -248,6 +248,7 @@
Clipping Mask Maska przycinająca
Edit Clipping Path Edytuj ścieżkę przycinającą
Compound Path Ścieżka złożona
Compound Shape Kształt złożony
Artboards Obszary kompozycji
Convert to Artboards Konwertuj na obszary kompozycji
Rearrange All Artboards… Przestaw wszystkie obszary kompozycji…
@@ -1752,7 +1753,6 @@ lighting Light Światło
Bottom-to-Bottom Leading Interlinia od dołu do dołu
Burasagari Burasagari
Force Wymuś
Expand applies to compound shapes (Alt-click a shape mode) — on the roadmap Rozszerzanie dotyczy kształtów złożonych (kliknij tryb kształtu z Alt) — w planach
Expand Compound Shape Rozszerz kształt złożony
Make Compound Shape Utwórz kształt złożony
Pathfinder Options… Opcje odnajdywania ścieżek…
@@ -3831,6 +3831,7 @@ font-technology Variable Zmienne
Default Fill and Stroke (D) Domyślne wypełnienie i obrys (D)
Fill (X), double-click for the Color Picker Wypełnienie (X), kliknij dwukrotnie, aby otworzyć próbnik kolorów
Opacity: Krycie:
Shape Mode Tryb kształtu
Shape Modes: Tryby kształtu:
Stroke (X), double-click for the Color Picker Obrys (X), kliknij dwukrotnie, aby otworzyć próbnik kolorów
Stroke: Obrys:
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2 -1
View File
@@ -514,6 +514,7 @@
Complexity: Complexidade:
Composite colours are converted to it; separations separate with it when it is a CMYK profile. As cores compostas são convertidas para ele; as separações separam com ele quando é um perfil CMYK.
Compound Path Caminho Composto
Compound Shape Forma Composta
Compress (RLE) Comprimir (RLE)
Compress text and line art Comprimir texto e arte de linha
Compression: Compressão:
@@ -884,7 +885,6 @@
Expand Panels Expandir Painéis
Expand Repeat Expandir Repetição
Expand Shape Expandir Forma
Expand applies to compound shapes (Alt-click a shape mode) — on the roadmap Expandir se aplica a formas compostas (Alt-clique em um modo de forma) — no roadmap
Expand: Expandir:
Expand… Expandir…
Export Exportar
@@ -2465,6 +2465,7 @@
Shape Builder Regions Regiões do Construtor de Formas
Shape Builder Tool Ferramenta Construtor de Formas
Shape Effects Efeitos de Forma
Shape Mode Modo de Forma
Shape Modes: Modos de Forma:
Shape Properties: Propriedades da Forma:
Shaper Tool Ferramenta Formador
Can't render this file because it contains an unexpected character in line 6 and column 47.
+2 -1
View File
@@ -563,6 +563,7 @@ font-technology Variable Вариативные
Complexity: Сложность:
Composite colours are converted to it; separations separate with it when it is a CMYK profile. Составные цвета преобразуются в него; цветоделения разделяются с ним, если это профиль CMYK.
Compound Path Составной контур
Compound Shape Составная фигура
Compress (RLE) Сжать (RLE)
Compress text and line art Сжимать текст и штриховой рисунок
Compression Сжатие
@@ -989,7 +990,6 @@ font-technology Variable Вариативные
Exit Isolation Mode Выйти из режима изоляции
Expand Разобрать
Expand Appearance Разобрать оформление
Expand applies to compound shapes (Alt-click a shape mode) — on the roadmap «Разобрать» применяется к составным фигурам (Alt+щелчок по режиму фигуры) — в плане развития
Expand Brush Strokes Разобрать мазки кисти
Expand Compound Shape Разобрать составную фигуру
Expand Gradient Mesh Разобрать градиентную сетку
@@ -3004,6 +3004,7 @@ mark-style Japanese Японский
Shape Builder Regions Области создания фигур
Shape Builder Tool Инструмент «Создание фигур»
Shape Effects Эффекты формы
Shape Mode Режим фигуры
Shape Modes: Режимы фигур:
Shape Properties: Свойства фигуры:
Shaper Tool Инструмент «Формообразование»
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2 -1
View File
@@ -552,6 +552,7 @@ font-technology Variable Variabla
Complexity: Komplexitet:
Composite colours are converted to it; separations separate with it when it is a CMYK profile. Sammansatta färger konverteras till den; separationer separeras med den när den är en CMYK-profil.
Compound Path Sammansatt bana
Compound Shape Sammansatt form
Compress (RLE) Komprimera (RLE)
Compress text and line art Komprimera text och linjegrafik
Compression Komprimering
@@ -976,7 +977,6 @@ font-technology Variable Variabla
Exit Isolation Mode Avsluta isoleringsläge
Expand Expandera
Expand Appearance Expandera utseende
Expand applies to compound shapes (Alt-click a shape mode) — on the roadmap Expandera gäller för sammansatta former (Alt-klicka på ett formläge) – planerat
Expand Brush Strokes Expandera penseldrag
Expand Compound Shape Expandera sammansatt form
Expand Gradient Mesh Expandera gradientnät
@@ -2904,6 +2904,7 @@ font-technology Variable Variabla
Shape Builder Regions Formbyggarområden
Shape Builder Tool Formbyggarverktyg
Shape Effects Figureffekter
Shape Mode Formläge
Shape Modes: Formlägen:
Shape Properties: Formegenskaper:
Shaper Tool Formningsverktyg
Can't render this file because it contains an unexpected character in line 5 and column 25.
+2 -1
View File
@@ -546,6 +546,7 @@ font-technology Variable Değişken
Complexity: Karmaşıklık:
Composite colours are converted to it; separations separate with it when it is a CMYK profile. Bileşik renkler buna dönüştürülür; CMYK profili olduğunda ayrımlar onunla ayrılır.
Compound Path Bileşik Yol
Compound Shape Bileşik Şekil
Compress (RLE) Sıkıştır (RLE)
Compress text and line art Metni ve çizgi resmini sıkıştır
Compression Sıkıştırma
@@ -968,7 +969,6 @@ font-technology Variable Değişken
Exit Isolation Mode İzolasyon Modundan Çık
Expand Genişlet
Expand Appearance Görünümü Genişlet
Expand applies to compound shapes (Alt-click a shape mode) — on the roadmap Genişletme, yol haritasındaki bileşik şekillere uygulanır (Alt tuşuyla bir şekil moduna tıklayın)
Expand Brush Strokes Fırça Darbelerini Genişlet
Expand Compound Shape Bileşik Şekli Genişlet
Expand Gradient Mesh Degrade Mesh'i Genişlet
@@ -2816,6 +2816,7 @@ shape Open Açık
Shape Builder Regions Şekil Oluşturucu Bölgeleri
Shape Builder Tool Şekil Oluşturucu Aracı
Shape Effects Şekil Efektleri
Shape Mode Şekil Modu
Shape Modes: Şekil Modları:
Shape Properties: Şekil Özellikleri:
Shaper Tool Şekillendirici Aracı
Can't render this file because it contains an unexpected character in line 5 and column 25.
+2 -1
View File
@@ -559,6 +559,7 @@ font-technology Variable Варіативні
Complexity: Складність:
Composite colours are converted to it; separations separate with it when it is a CMYK profile. Складові кольори перетворюються за цим профілем; кольороподіл також виконується за ним, якщо це профіль CMYK.
Compound Path Складений контур
Compound Shape Складена фігура
Compress (RLE) Стиснути (RLE)
Compress text and line art Стискати текст та штриховий малюнок
Compression Стиснення
@@ -985,7 +986,6 @@ font-technology Variable Варіативні
Exit Isolation Mode Вийти з режиму ізоляції
Expand Розібрати
Expand Appearance Розібрати оформлення
Expand applies to compound shapes (Alt-click a shape mode) — on the roadmap «Розібрати» застосовується до складених фігур (Alt+клацання за режимом фігури) — у плані розвитку
Expand Brush Strokes Розібрати мазки пензля
Expand Compound Shape Розібрати складену фігуру
Expand Gradient Mesh Розібрати градієнтну сітку
@@ -3000,6 +3000,7 @@ mark-style Japanese Японський
Shape Builder Regions Області створення фігур
Shape Builder Tool Інструмент «Створення фігур»
Shape Effects Ефекти форми
Shape Mode Режим фігури
Shape Modes: Режими фігур:
Shape Properties: Властивості фігури:
Shaper Tool Інструмент «Формоутворення»
Can't render this file because it contains an unexpected character in line 178 and column 5.
+2 -1
View File
@@ -653,6 +653,7 @@ font-technology Variable Biến thiên
Complexity: Độ phức tạp:
Composite colours are converted to it; separations separate with it when it is a CMYK profile. Màu tổng hợp được chuyển sang đây; tách màu sẽ tách với nó khi là hồ sơ CMYK.
Compound Path Đường dẫn phức
Compound Shape Hình phức hợp
Compress (RLE) Nén (RLE)
Compress text and line art Nén văn bản và đường nét
Compression Nén
@@ -1148,7 +1149,6 @@ font-technology Variable Biến thiên
Exit Isolation Mode Thoát chế độ cô lập
Expand Mở rộng
Expand Appearance Mở rộng diện mạo
Expand applies to compound shapes (Alt-click a shape mode) — on the roadmap Mở rộng áp dụng cho hình ghép (Alt-bấm chế độ hình dạng) — trong lộ trình
Expand Brush Strokes Mở rộng nét cọ
Expand Compound Shape Mở rộng hình ghép
Expand Gradient Mesh Mở rộng lưới chuyển sắc
@@ -3328,6 +3328,7 @@ mark-style Japanese Kiểu Nhật
Shape Builder Regions Vùng Tạo hình dạng
Shape Builder Tool Công cụ Tạo hình dạng
Shape Effects Hiệu ứng hình dạng
Shape Mode Chế độ hình dạng
Shape Modes: Chế độ hình dạng:
Shape Properties: Thuộc tính hình dạng:
Shape: Hình dạng:
Can't render this file because it contains an unexpected character in line 200 and column 5.
+2 -1
View File
@@ -511,6 +511,7 @@
Complexity: 复杂度:
Composite colours are converted to it; separations separate with it when it is a CMYK profile. 复合颜色会转换为此配置文件;其为 CMYK 配置文件时,分色也会使用它。
Compound Path 复合路径
Compound Shape 复合形状
Compress (RLE) 压缩 (RLE)
Compress text and line art 压缩文字与线条图
Compression 压缩
@@ -933,7 +934,6 @@
Exit Isolation Mode 退出隔离模式
Expand 扩展
Expand Appearance 扩展外观
Expand applies to compound shapes (Alt-click a shape mode) — on the roadmap 扩展仅适用于复合形状 (Alt+单击形状模式)——已在路线图中
Expand Brush Strokes 扩展画笔描边
Expand Compound Shape 扩展复合形状
Expand Gradient Mesh 扩展渐变网格
@@ -2841,6 +2841,7 @@ font-script Japanese 汉字
Shape Builder Regions 形状生成器区域
Shape Builder Tool 形状生成器工具
Shape Effects 形状效果
Shape Mode 形状模式
Shape Modes: 形状模式:
Shape Properties: 形状属性:
Shaper Tool Shaper 工具
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2 -1
View File
@@ -527,6 +527,7 @@ font-technology Variable 可變
Complexity: 複雜度:
Composite colours are converted to it; separations separate with it when it is a CMYK profile. 複合顏色會轉換為此描述檔;其為 CMYK 描述檔時,分色也會使用它。
Compound Path 複合路徑
Compound Shape 複合形狀
Compress (RLE) 壓縮 (RLE)
Compress text and line art 壓縮文字與線條圖
Compression 壓縮
@@ -955,7 +956,6 @@ font-technology Variable 可變
Exit Isolation Mode 結束隔離模式
Expand 展開
Expand Appearance 展開外觀
Expand applies to compound shapes (Alt-click a shape mode) — on the roadmap 展開適用於複合形狀 (按住 Alt 按一下形狀模式),已列入開發計畫
Expand Brush Strokes 展開筆刷筆畫
Expand Compound Shape 展開複合形狀
Expand Gradient Mesh 展開漸層網格
@@ -3812,6 +3812,7 @@ font-script Japanese 漢字
Shape Builder Regions 形狀建立區域
Shape Builder Tool 形狀建立工具
Shape Effects 形狀效果
Shape Mode 形狀模式
Shape Modes: 形狀模式:
Shape Properties: 形狀內容:
Shaper Tool Shaper 工具
Can't render this file because it contains an unexpected character in line 6 and column 47.
+74 -19
View File
@@ -1,7 +1,10 @@
//! Pathfinder panel: Shape Modes (+ Expand) and Pathfinders, with icons drawn for VectorCraft.
//! Alt-clicking a Shape Mode makes a compound shape; with members of one selected, a Shape Mode
//! sets theirs.
use egui::Ui;
use serde_json::json;
use vectorcraft_engine::doc::ShapeMode;
use super::{pstate, selection_len, set_pstate};
use crate::VectorcraftApp;
@@ -39,9 +42,10 @@ fn operands(app: &VectorcraftApp) -> usize {
}
}
/// An operation's button, greyed out and saying what it needs unless the selection has `min` operands.
fn button(ui: &mut Ui, icon: &str, tip: &str, n: usize, min: usize) -> bool {
let resp = widgets::icon_button_enabled(ui, icon, tip, false, n >= min, 34.0);
/// An operation's button, lit while `selected`, greyed out and saying what it needs unless the
/// selection has `min` operands.
fn button(ui: &mut Ui, icon: &str, tip: &str, selected: bool, n: usize, min: usize) -> bool {
let resp = widgets::icon_button_enabled(ui, icon, tip, selected, n >= min, 34.0);
if n < min {
resp.on_hover_text(tl!("Select two or more objects, or a group"));
return false;
@@ -49,31 +53,46 @@ fn button(ui: &mut Ui, icon: &str, tip: &str, n: usize, min: usize) -> bool {
resp.clicked()
}
/// A Shape Mode button clicked: Alt makes a compound shape of the selection in `mode`; with
/// members of compound shapes selected they take `mode`; otherwise operation `op` runs.
fn shape_mode(app: &mut VectorcraftApp, ui: &Ui, op: &str, label: &str, mode: ShapeMode, members: bool) {
if ui.input(|i| i.modifiers.alt) {
crate::menus::invoke(app, "object.compoundShape.make", json!({"mode": mode.name()}));
} else if members {
crate::menus::invoke(app, "object.compoundShape.setMode", json!({"mode": mode.name()}));
} else {
run(app, ui, op, label);
}
}
pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
let n = operands(app);
let t = crate::theme::Tokens::get(ui.ctx());
// The modes of the compound-shape members selected: their Shape Mode button lights when they
// share one.
let modes = vectorcraft_engine::cmd::selected_member_modes(&app.session);
let shared = modes.first().copied().filter(|m| modes.iter().all(|x| x == m));
let expand = crate::menus::enabled(app, "object.compoundShape.expand");
widgets::subheader(ui, tl!("Shape Modes:"));
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 5.0;
for (icon, tip, op) in SHAPE_MODES {
if button(ui, icon, tip, n, 2) {
run(app, ui, op, tip);
// Selected members can always take a mode.
let count = if modes.is_empty() { n } else { n.max(2) };
for ((icon, tip, op), mode) in SHAPE_MODES.into_iter().zip(ShapeMode::ALL) {
if button(ui, icon, tip, shared == Some(mode), count, 2) {
shape_mode(app, ui, op, tip, mode, !modes.is_empty());
}
}
ui.add_enabled_ui(false, |ui| {
let (r, _) = ui.allocate_exact_size(egui::vec2(ui.available_width().min(90.0), 26.0), egui::Sense::hover());
ui.painter().rect_filled(r, 2, t.hover.gamma_multiply(0.5));
ui.painter().text(r.center(), egui::Align2::CENTER_CENTER, tl!("Expand"), egui::FontId::proportional(12.5), t.text_disabled);
})
.response
.on_disabled_hover_text(tl!("Expand applies to compound shapes (Alt-click a shape mode) — on the roadmap"));
let resp = ui.add_enabled_ui(expand, |ui| widgets::flat_button_fit(ui, "Expand", 90.0)).inner;
if resp.on_hover_text(tl!("Expand Compound Shape")).on_disabled_hover_text(tl!("Expand Compound Shape")).clicked() {
crate::menus::invoke(app, "object.compoundShape.expand", json!({}));
}
});
widgets::subheader(ui, tl!("Pathfinders:"));
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 5.0;
for (icon, tip, op) in PATHFINDERS {
let min = if op == "outline" || op == "divide" { 1 } else { 2 };
if button(ui, icon, tip, n, min) {
if button(ui, icon, tip, false, n, min) {
run(app, ui, op, tip);
}
}
@@ -103,15 +122,21 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
crate::menus::invoke(app, "object.pathfinder.options", json!({}));
}
ui.separator();
menu_item(ui, tl!("Make Compound Shape"), false, false);
menu_item(ui, tl!("Release Compound Shape"), false, false);
menu_item(ui, tl!("Expand Compound Shape"), false, false);
for (label, id) in [
(tl!("Make Compound Shape"), "object.compoundShape.make"),
(tl!("Release Compound Shape"), "object.compoundShape.release"),
(tl!("Expand Compound Shape"), "object.compoundShape.expand"),
] {
if menu_item(ui, label, crate::menus::enabled(app, id), false) {
crate::menus::invoke(app, id, json!({}));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::widgets::accessibility_tests::{named, tree};
use crate::widgets::accessibility_tests::{named, role_and_state, tree};
use serde_json::Value;
use vectorcraft_engine::doc::{NodeId, NodeKind};
@@ -155,6 +180,36 @@ mod tests {
}
}
/// The panel menu's compound shape items: Make with two or more objects selected, Release and
/// Expand with a compound shape (or a member of one); the Expand button follows Expand, and
/// with a member selected its Shape Mode button is lit.
#[test]
fn compound_shape_items_follow_the_selection() {
use egui::accesskit::{Role, Toggled};
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
app.run("file.new", json!({"width": 200, "height": 200})).unwrap();
let a = app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap()["id"].clone();
let b = app.run("shape.rectangle", json!({"x": 40, "y": 40, "width": 50, "height": 50})).unwrap()["id"].clone();
let state = |app: &mut VectorcraftApp| {
let items = tree(|ui| menu(app, ui));
let on = |label: &str| !named(&items, &format!(" {label}")).is_disabled();
let panel = tree(|ui| show(app, ui));
(on("Make Compound Shape"), on("Release Compound Shape"), on("Expand Compound Shape"), !named(&panel, "Expand").is_disabled())
};
app.run("select.set", json!({"ids": [a]})).unwrap();
assert_eq!(state(&mut app), (false, false, false, false), "one object");
app.run("select.set", json!({"ids": [a, b]})).unwrap();
assert_eq!(state(&mut app), (true, false, false, false), "two objects");
app.run("object.compoundShape.make", json!({"mode": "subtract"})).unwrap();
assert_eq!(state(&mut app), (false, true, true, true), "a compound shape");
app.run("select.set", json!({"ids": [b]})).unwrap();
assert_eq!(state(&mut app), (false, true, true, true), "a member");
let panel = tree(|ui| show(&mut app, ui));
assert_eq!(role_and_state(&panel, "Minus Front"), (Role::Button, Some(Toggled::True)), "the member's mode");
assert!(!named(&panel, "Unite").is_disabled(), "a member takes another mode");
assert_ne!(role_and_state(&panel, "Unite").1, Some(Toggled::True));
}
/// The panel menu's Trap… is enabled for two objects and opens the Trap options; OK adds the
/// trap above them as one selected group in one undo step.
#[test]
+3 -1
View File
File diff suppressed because one or more lines are too long
+17
View File
@@ -2199,6 +2199,23 @@ whatever the options are then.
{"name":"run_command","arguments":{"command":"object.pathfinder.options","params":{"precision":0.1,"removeUnpainted":true}}}
```
Compound shapes (the Pathfinder panel menu, and Alt-clicking a Shape Mode button): `object.compoundShape.make
{mode?, ids?}` makes the two or more selected objects (paths, compound paths, groups, compound shapes, type) the
members of one live compound shape, each in shape mode `mode` (`"add"`, the default, `"subtract"`, `"intersect"` or
`"exclude"`). It draws, prints and exports as the area its members make from the backmost up, painted with its own
appearance, which it takes from the topmost member not in Subtract (with every member subtracting, the backmost); a
fill or stroke given to it or to a member paints the compound shape. The members stay editable (Direct Selection,
the Layers panel, isolation mode). `object.compoundShape.setMode {mode, ids?}` sets the mode of the members the
selection is or is in, `object.compoundShape.release` gives the members back as they were, and
`object.compoundShape.expand` turns each compound shape into the path, or compound path, of its area. A saved file
keeps the compound shape (`compound_shape` on the group, `shape_mode` on each member); a mode the file doesn't name
reads as Add.
```json
{"name":"run_command","arguments":{"command":"object.compoundShape.make","params":{"mode":"subtract"}}}
{"name":"run_command","arguments":{"command":"object.compoundShape.expand","params":{}}}
```
The Eyedropper: `appearance.copyFrom {source}` copies what the Eyedropper Options pick up and apply from `source` to the
selection (`ids`); `reverse: true` copies the selection's attributes onto `source` (Alt-click), or with nothing selected
what the last copy without `reverse` picked up (a copy made then, kept for the session), and `append: true` adds
+3 -2
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+3 -1
View File
File diff suppressed because one or more lines are too long