mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-14 22:35:26 +00:00
Merge branch 'object-styles-panel' into object-style-paragraph-styles
# Conflicts: # ROADMAP.md # crates/engine/src/cmd/style.rs # crates/idml/src/import.rs
This commit is contained in:
commit
38443c0b86
7 files changed
+129
-36
No files matched your search
@@ -64,7 +64,9 @@ pub struct ObjectStyle {
|
||||
/// effects differ from the style's (a local override).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub effects: Option<Effects>,
|
||||
/// Corner Options (Stroke & Corner Options): each corner's shape and size, in path order.
|
||||
/// Corner Options (Stroke & Corner Options): each corner's shape and size, by name (top left,
|
||||
/// top right, bottom right, bottom left), as InDesign's; an item takes them at the anchors at
|
||||
/// those corners ([`CornerOptions::named_to_path`]).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub corners: Option<CornerOptions>,
|
||||
}
|
||||
@@ -73,7 +75,7 @@ impl ObjectStyle {
|
||||
/// Gives `it` the fill, stroke, corners, effects and text frame options this style sets.
|
||||
pub fn apply_to(&self, it: &mut Item) {
|
||||
if let Some(c) = self.corners {
|
||||
it.corners = c;
|
||||
it.corners = c.named_to_path(&it.path);
|
||||
}
|
||||
if let Some(f) = &self.fill {
|
||||
it.fill = f.clone();
|
||||
|
||||
@@ -1527,7 +1527,7 @@ fn create_object(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if let Some(it) = &src {
|
||||
os.fill = Some(it.fill.clone());
|
||||
os.stroke = Some(it.stroke.clone());
|
||||
os.corners = Some(it.corners);
|
||||
os.corners = Some(it.corners.path_to_named(&it.path));
|
||||
os.text_frame = it.text_frame().map(|t| t.options.clone());
|
||||
os.effects = Some(it.effects.clone());
|
||||
}
|
||||
@@ -1761,7 +1761,7 @@ fn edit_object(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
effects.as_ref().is_some_and(|(o, _)| it.effects == *o)
|
||||
|| fill.as_ref().is_some_and(|(o, _)| it.fill == *o)
|
||||
|| stroke.as_ref().is_some_and(|(o, _)| it.stroke == *o)
|
||||
|| corners.as_ref().is_some_and(|(o, _)| it.corners == *o)
|
||||
|| corners.as_ref().is_some_and(|(o, _)| it.corners == o.named_to_path(&it.path))
|
||||
|| frame.as_ref().is_some_and(|(o, _)| it.text_frame().is_some_and(|t| t.options == *o))
|
||||
};
|
||||
let updated = d.update_items(
|
||||
@@ -1783,9 +1783,9 @@ fn edit_object(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
it.stroke = n.clone();
|
||||
}
|
||||
if let Some((o, n)) = &corners
|
||||
&& it.corners == *o
|
||||
&& it.corners == o.named_to_path(&it.path)
|
||||
{
|
||||
it.corners = *n;
|
||||
it.corners = n.named_to_path(&it.path);
|
||||
}
|
||||
if let Some((o, n)) = &frame
|
||||
&& let Some(tf) = it.text_frame_mut()
|
||||
|
||||
@@ -55,10 +55,53 @@ pub struct CornerOptions {
|
||||
pub corners: [Corner; 4],
|
||||
}
|
||||
|
||||
/// The named corner (0 top left, 1 top right, 2 bottom right, 3 bottom left) each of a path's
|
||||
/// four anchors is, by its place on the path's bounds. A path that isn't one subpath of four
|
||||
/// anchors at four different corners keeps a rectangle's order.
|
||||
pub fn named_slots(path: &PathData) -> [usize; 4] {
|
||||
let rect_order = [0, 1, 2, 3];
|
||||
let Some(sp) = path.subpaths.first() else { return rect_order };
|
||||
let Some(b) = path.bounds() else { return rect_order };
|
||||
if sp.anchors.len() != 4 {
|
||||
return rect_order;
|
||||
}
|
||||
let c = b.center();
|
||||
let mut out = rect_order;
|
||||
let mut seen = [false; 4];
|
||||
for (i, a) in sp.anchors.iter().enumerate() {
|
||||
let k = match (a.p.y < c.y, a.p.x < c.x) {
|
||||
(true, true) => 0,
|
||||
(true, false) => 1,
|
||||
(false, false) => 2,
|
||||
(false, true) => 3,
|
||||
};
|
||||
if std::mem::replace(&mut seen[k], true) {
|
||||
return rect_order;
|
||||
}
|
||||
out[i] = k;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
impl CornerOptions {
|
||||
pub fn uniform(shape: CornerShape, size: f64) -> Self {
|
||||
Self { corners: [Corner { shape, size }; 4] }
|
||||
}
|
||||
|
||||
/// Named corners (top left, top right, bottom right, bottom left), as an object style holds
|
||||
/// them, in `path`'s order.
|
||||
pub fn named_to_path(&self, path: &PathData) -> Self {
|
||||
Self { corners: named_slots(path).map(|k| self.corners[k]) }
|
||||
}
|
||||
|
||||
/// `path`-order corners as named corners (top left, top right, bottom right, bottom left).
|
||||
pub fn path_to_named(&self, path: &PathData) -> Self {
|
||||
let mut corners = self.corners;
|
||||
for (i, k) in named_slots(path).into_iter().enumerate() {
|
||||
corners[k] = self.corners[i];
|
||||
}
|
||||
Self { corners }
|
||||
}
|
||||
pub fn is_none(&self) -> bool {
|
||||
self.corners.iter().all(Corner::is_none)
|
||||
}
|
||||
@@ -252,6 +295,30 @@ mod tests {
|
||||
use crate::shapes::rectangle;
|
||||
use kurbo::{Rect, Shape};
|
||||
|
||||
/// Named corners go to the anchors at those corners, whatever order the path runs in, and
|
||||
/// come back the same.
|
||||
#[test]
|
||||
fn named_corners_map_to_any_path_order() {
|
||||
use kurbo::Point;
|
||||
let named = CornerOptions {
|
||||
corners: [CornerShape::Rounded, CornerShape::Bevel, CornerShape::Inset, CornerShape::Fancy].map(|shape| Corner { shape, size: 6.0 }),
|
||||
};
|
||||
// Top left, bottom left, bottom right, top right (as InDesign writes a rectangle).
|
||||
let down_first = PathData::single(SubPath::polyline(
|
||||
&[Point::new(0.0, 0.0), Point::new(0.0, 100.0), Point::new(100.0, 100.0), Point::new(100.0, 0.0)],
|
||||
true,
|
||||
));
|
||||
assert_eq!(named_slots(&down_first), [0, 3, 2, 1]);
|
||||
let on_path = named.named_to_path(&down_first);
|
||||
let shapes = on_path.corners.map(|c| c.shape);
|
||||
assert_eq!(shapes, [CornerShape::Rounded, CornerShape::Fancy, CornerShape::Inset, CornerShape::Bevel]);
|
||||
assert_eq!(on_path.path_to_named(&down_first), named);
|
||||
// A rectangle in its own order, and a triangle, keep the order.
|
||||
assert_eq!(named.named_to_path(&rectangle(Rect::new(0.0, 0.0, 10.0, 10.0))), named);
|
||||
let tri = PathData::single(SubPath::polyline(&[Point::new(0.0, 0.0), Point::new(10.0, 0.0), Point::new(5.0, 9.0)], true));
|
||||
assert_eq!(named.named_to_path(&tri), named);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn none_is_identity() {
|
||||
let r = rectangle(Rect::new(0.0, 0.0, 100.0, 50.0));
|
||||
|
||||
@@ -3021,28 +3021,7 @@ fn path_geometry(path: &PathData) -> El {
|
||||
|
||||
/// IDML corner names for anchors 0..4 of a path (by position on its bounding box).
|
||||
pub(crate) fn corner_names(path: &PathData) -> [&'static str; 4] {
|
||||
let default = ["TopLeft", "TopRight", "BottomRight", "BottomLeft"];
|
||||
let Some(sp) = path.subpaths.first() else { return default };
|
||||
if sp.anchors.len() != 4 {
|
||||
return default;
|
||||
}
|
||||
let Some(b) = path.bounds() else { return default };
|
||||
let c = b.center();
|
||||
let mut out = default;
|
||||
let mut seen = HashSet::new();
|
||||
for (i, a) in sp.anchors.iter().enumerate() {
|
||||
let n = match (a.p.y < c.y, a.p.x < c.x) {
|
||||
(true, true) => "TopLeft",
|
||||
(true, false) => "TopRight",
|
||||
(false, false) => "BottomRight",
|
||||
(false, true) => "BottomLeft",
|
||||
};
|
||||
if !seen.insert(n) {
|
||||
return default;
|
||||
}
|
||||
out[i] = n;
|
||||
}
|
||||
out
|
||||
designcraft_geom::corners::named_slots(path).map(|k| ["TopLeft", "TopRight", "BottomRight", "BottomLeft"][k])
|
||||
}
|
||||
|
||||
fn format_name(mime: &str) -> &'static str {
|
||||
|
||||
@@ -471,11 +471,18 @@ fn parse_point(s: Option<&str>) -> Option<Point> {
|
||||
const INDESIGN_CORNER_SIZE: f64 = 12.0;
|
||||
|
||||
/// The four corners of a paragraph border or shading (`prefix` `ParagraphBorder` /
|
||||
/// `ParagraphShading`): top left, top right, bottom right, bottom left. Unset when none is given.
|
||||
/// `ParagraphShading`), or an object style (no prefix): top left, top right, bottom right,
|
||||
/// bottom left. Unset when none is given.
|
||||
fn corners(e: &El, prefix: &str) -> Option<[Corner; 4]> {
|
||||
corners_in(&[e], prefix)
|
||||
}
|
||||
|
||||
/// [`corners`] over `levels` (an object style, then the styles it is based on): each corner's
|
||||
/// shape and size from the first level that gives it.
|
||||
fn corners_in(levels: &[&El], prefix: &str) -> Option<[Corner; 4]> {
|
||||
let keys = ["TopLeft", "TopRight", "BottomRight", "BottomLeft"];
|
||||
let shapes = keys.map(|n| e.prop(&format!("{prefix}{n}CornerOption")));
|
||||
let sizes = keys.map(|n| e.num(&format!("{prefix}{n}CornerRadius")));
|
||||
let shapes = keys.map(|n| levels.iter().find_map(|e| e.prop(&format!("{prefix}{n}CornerOption"))));
|
||||
let sizes = keys.map(|n| levels.iter().find_map(|e| e.num(&format!("{prefix}{n}CornerRadius"))));
|
||||
if shapes.iter().all(Option::is_none) && sizes.iter().all(Option::is_none) {
|
||||
return None;
|
||||
}
|
||||
@@ -1247,6 +1254,7 @@ impl<'r> Importer<'r> {
|
||||
None => self.styles.table.push(ts),
|
||||
}
|
||||
}
|
||||
let objects_by_self: HashMap<&str, &El> = objects.iter().filter_map(|(_, e)| Some((e.get("Self")?, e))).collect();
|
||||
for (_, e) in &objects {
|
||||
let name = names::style_name_in(OBJECT_BUILTINS, e.get("Name").unwrap_or(""));
|
||||
let based_on = e.prop("BasedOn").map(|b| {
|
||||
@@ -1274,10 +1282,25 @@ impl<'r> Importer<'r> {
|
||||
None
|
||||
};
|
||||
let effects = self.style_effects(e);
|
||||
// Corner options, when the style sets them (Stroke & Corner Options): the named corners
|
||||
// in a rectangle's path order.
|
||||
let corners =
|
||||
(e.get("EnableStrokeAndCornerOptions") != Some("false")).then(|| corners(e, "")).flatten().map(|c| CornerOptions { corners: c });
|
||||
// Corner options, when the style sets them (Stroke & Corner Options), by name. A style
|
||||
// that sets some corners takes the others from the styles it is based on, as the style
|
||||
// holds all four.
|
||||
let corners = (e.get("EnableStrokeAndCornerOptions") != Some("false") && corners(e, "").is_some())
|
||||
.then(|| {
|
||||
let mut levels = vec![e];
|
||||
let mut next = e.prop("BasedOn");
|
||||
while let Some(b) = next.take()
|
||||
&& levels.len() < 16
|
||||
&& let Some(s) = objects_by_self.get(b.trim()).copied()
|
||||
&& !levels.iter().any(|l| std::ptr::eq(*l, s))
|
||||
{
|
||||
levels.push(s);
|
||||
next = s.prop("BasedOn");
|
||||
}
|
||||
corners_in(&levels, "")
|
||||
})
|
||||
.flatten()
|
||||
.map(|c| CornerOptions { corners: c });
|
||||
let apply_next_style = paragraph_style.is_some() && e.get("ApplyNextParagraphStyle") == Some("true");
|
||||
let s = ObjectStyle { name: name.clone(), based_on, fill, stroke, paragraph_style, apply_next_style, text_frame, effects, corners };
|
||||
match self.styles.object.iter_mut().find(|o| o.name == name) {
|
||||
|
||||
@@ -255,3 +255,24 @@ fn feathers_come_from_the_object_style_unless_its_category_turns_them_off() {
|
||||
assert!(fx[2].gradient_feather.on);
|
||||
assert!(!fx[2].directional_feather.on, "the item's own setting wins");
|
||||
}
|
||||
|
||||
/// A style that sets one corner takes the others from the style it is based on, so applying it
|
||||
/// keeps the based-on style's rounded corners; and a style's named corners go to the anchors at
|
||||
/// those corners of a frame whose path runs top left, bottom left, bottom right, top right.
|
||||
#[test]
|
||||
fn a_style_setting_one_corner_inherits_the_others_and_names_them() {
|
||||
let child = r#"<ObjectStyle Self="ObjectStyle/Child" Name="Child" TopRightCornerOption="BevelCorner" TopRightCornerRadius="4">
|
||||
<Properties><BasedOn type="object">ObjectStyle/Round</BasedOn></Properties>
|
||||
</ObjectStyle>"#;
|
||||
let d = fixture(&format!("{ROUND}{child}"), &[(r#"AppliedObjectStyle="ObjectStyle/Child""#, "")]);
|
||||
let style = d.styles.object.iter().find(|s| s.name == "Child").unwrap();
|
||||
let c = style.corners.expect("the child sets corners").corners;
|
||||
assert_eq!(c.map(|c| c.shape), [CornerShape::Rounded, CornerShape::Bevel, CornerShape::Rounded, CornerShape::Rounded], "{c:?}");
|
||||
assert_eq!(c.map(|c| c.size), [17.0, 4.0, 17.0, 17.0]);
|
||||
// Applied to the fixture's frame (anchors top left, bottom left, bottom right, top right):
|
||||
// the bevel goes to the top right anchor, the last.
|
||||
let mut it = (*d.spreads[0].items[0]).clone();
|
||||
it.corners = Default::default();
|
||||
style.apply_to(&mut it);
|
||||
assert_eq!(it.corners.corners.map(|c| c.shape), [CornerShape::Rounded, CornerShape::Rounded, CornerShape::Rounded, CornerShape::Bevel]);
|
||||
}
|
||||
@@ -56,7 +56,8 @@ pub fn open_new(app: &mut DesignApp) {
|
||||
"basedOn": designcraft_doc::NO_OBJECT_STYLE,
|
||||
"fill": it.as_ref().map(|i| i.fill.clone()).unwrap_or_default(),
|
||||
"stroke": it.as_ref().map(|i| i.stroke.clone()).unwrap_or_default(),
|
||||
"corners": it.as_ref().map(|i| i.corners).unwrap_or_default(),
|
||||
// A style holds named corners (top left, top right, …), the item its path's.
|
||||
"corners": it.as_ref().map(|i| i.corners.path_to_named(&i.path)).unwrap_or_default(),
|
||||
"fx": crate::effects_dialog::effects_json(&it.map(|i| i.effects).unwrap_or_default()),
|
||||
"hadEffects": from,
|
||||
"inc": {"fill": from, "stroke": from, "corners": from, "paragraph": from, "textFrame": frame.is_some()},
|
||||
|
||||
Reference in new issue
Block a user