mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-29 20:27:05 -06:00
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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::{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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!({})),
|
||||
|
||||
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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
File diff suppressed because one or more lines are too long
+17
@@ -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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+3
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user