mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-30 13:16:56 -06:00
This commit is contained in:
@@ -169,6 +169,7 @@ Newest first.
|
||||
- **2026-10-14** · Colour parameters: a bare `{c,m,y,k}` is always read as 0..100, so C 1 is a 1 % tint and no longer
|
||||
solid cyan; fractions take the colour's own `{model: "cmyk", …}` form (#910). No readiness percentage change.
|
||||
- **2026-10-14** · The Tools panel follows InDesign's: Gradient Swatch and Gradient Feather are tools of their own; in two columns Content Collector and Placer, Apply Color / Gradient / None and Normal / Preview each get a button; View Options holds Frame Edges, Rulers, Guides, Smart Guides, Baseline Grid and Hidden Characters; Apply Color (,) and Apply Gradient (.) reuse the last colour and gradient. No readiness percentage change.
|
||||
- **2026-10-13** · Gradient swatches work on strokes: drawn across the path's box on screen and in PDF (they drew nothing), set with `object.gradient {target: stroke}` and in the Gradient panel when the Stroke box is in front; the stroke's gradient vector moves with the shape and round-trips through IDML. No readiness percentage change.
|
||||
- **2026-10-12** · Text threads by hand: click a selected text frame's in or out port to load
|
||||
the text cursor, then click a frame to thread it, click empty space for a column-wide frame or
|
||||
drag one (it snaps like frame drawing); Shift-click autoflows, Alt-click stays loaded on the
|
||||
|
||||
@@ -54,6 +54,21 @@ impl Fill {
|
||||
}
|
||||
}
|
||||
|
||||
/// A linear gradient's axis on an object whose bounds (in its own space) are `b`: the
|
||||
/// Gradient Swatch tool's vector, or the box's extent along the gradient's angle through its
|
||||
/// centre. A box with no extent along that angle (a straight line, across it) takes its longer
|
||||
/// side.
|
||||
pub fn linear_axis(&self, b: Rect) -> (Point, Point) {
|
||||
if let Some([x0, y0, x1, y1]) = self.gradient_vector {
|
||||
return (Point::new(x0, y0), Point::new(x1, y1));
|
||||
}
|
||||
let a = self.gradient_angle.unwrap_or(0.0).to_radians();
|
||||
let half = (b.width() * a.cos().abs() + b.height() * a.sin().abs()) / 2.0;
|
||||
let half = if half > 1e-9 { half } else { b.width().max(b.height()).max(1e-3) / 2.0 };
|
||||
let d = designcraft_geom::Vec2::new(a.cos(), -a.sin()) * half;
|
||||
(b.center() - d, b.center() + d)
|
||||
}
|
||||
|
||||
pub fn none() -> Self {
|
||||
Fill { swatch: designcraft_color::swatch::NONE.into(), tint: 1.0, gradient_angle: None, gradient_vector: None, overprint: false }
|
||||
}
|
||||
@@ -246,6 +261,12 @@ pub struct Stroke {
|
||||
/// Overprint Gap (Attributes panel).
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub gap_overprint: bool,
|
||||
/// A gradient swatch's angle on the stroke, as [`Fill::gradient_angle`].
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub gradient_angle: Option<f64>,
|
||||
/// The Gradient Swatch tool's vector on the stroke, as [`Fill::gradient_vector`].
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub gradient_vector: Option<[f64; 4]>,
|
||||
}
|
||||
|
||||
impl Default for Stroke {
|
||||
@@ -266,11 +287,25 @@ impl Default for Stroke {
|
||||
gap_tint: 1.0,
|
||||
overprint: false,
|
||||
gap_overprint: false,
|
||||
gradient_angle: None,
|
||||
gradient_vector: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Stroke {
|
||||
/// The stroke's paint (swatch, tint and gradient geometry) as a fill, to paint it the same way:
|
||||
/// a gradient swatch on a stroke runs across the object's box as on its fill.
|
||||
pub fn as_fill(&self) -> Fill {
|
||||
Fill {
|
||||
swatch: self.swatch.clone(),
|
||||
tint: self.tint,
|
||||
gradient_angle: self.gradient_angle,
|
||||
gradient_vector: self.gradient_vector,
|
||||
overprint: self.overprint,
|
||||
}
|
||||
}
|
||||
|
||||
/// How far the painted stroke can reach beyond the path (arrowheads reach further).
|
||||
pub fn extent(&self) -> f64 {
|
||||
if self.start == Arrowhead::None && self.end == Arrowhead::None { self.weight } else { self.weight.max(0.5) * 5.5 }
|
||||
|
||||
@@ -1140,12 +1140,15 @@ fn union_bounds(d: &Document, ids: &[ItemId]) -> Rect {
|
||||
ids.iter().filter_map(|i| d.item(*i)).map(Item::bounds).reduce(|a, b| a.union(b)).unwrap_or(Rect::ZERO)
|
||||
}
|
||||
|
||||
/// Transform an item's geometry in its own space: the path and the gradient vector with it.
|
||||
/// Transform an item's geometry in its own space: the path and the gradient vectors (fill and
|
||||
/// stroke) with it.
|
||||
pub(crate) fn bake(it: &mut Item, inner: Affine) {
|
||||
it.path.transform(inner);
|
||||
if let Some([x0, y0, x1, y1]) = it.fill.gradient_vector {
|
||||
let (a, b) = (inner * Point::new(x0, y0), inner * Point::new(x1, y1));
|
||||
it.fill.gradient_vector = Some([a.x, a.y, b.x, b.y]);
|
||||
for v in [&mut it.fill.gradient_vector, &mut it.stroke.gradient_vector] {
|
||||
if let Some([x0, y0, x1, y1]) = *v {
|
||||
let (a, b) = (inner * Point::new(x0, y0), inner * Point::new(x1, y1));
|
||||
*v = Some([a.x, a.y, b.x, b.y]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Gradient",
|
||||
[],
|
||||
None,
|
||||
"{kind?: linear|radial, stops?: [{location: 0–100, color: \"#rrggbb\"|{c,m,y,k}(0..100)|[r,g,b]|{model, …}(0..1), opacity?: 0–100, midpoint?: 13–87}], reverse?: bool, angle?: degrees, from?: [x,y], to?: [x,y] (spread coords: the Gradient Swatch tool's drag), ids?} → {swatch, kind, stops} — edits the fill's gradient (an unnamed gradient unless it equals a gradient swatch)",
|
||||
"{kind?: linear|radial, stops?: [{location: 0–100, color: \"#rrggbb\"|{c,m,y,k}(0..100)|[r,g,b]|{model, …}(0..1), opacity?: 0–100, midpoint?: 13–87}], reverse?: bool, angle?: degrees, from?: [x,y], to?: [x,y] (spread coords: the Gradient Swatch tool's drag), target?: fill|stroke, ids?} → {swatch, kind, stops} — edits the fill's (or the stroke's) gradient (an unnamed gradient unless it equals a gradient swatch)",
|
||||
super::has_selection,
|
||||
apply_gradient
|
||||
),
|
||||
@@ -926,13 +926,18 @@ fn apply_gradient(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
};
|
||||
let reverse = p.get("reverse").and_then(Value::as_bool).unwrap_or(false);
|
||||
let angle = p.get("angle").and_then(Value::as_f64);
|
||||
let stroke = match str_param(p, "target") {
|
||||
None | Some("fill") => false,
|
||||
Some("stroke") => true,
|
||||
Some(t) => return Err(bad("object.gradient", format!("unknown target {t}"))),
|
||||
};
|
||||
let (from, to) = (super::point_param(p, "from"), super::point_param(p, "to"));
|
||||
let ids = super::targets(s, p)?;
|
||||
s.edit(|d, _| {
|
||||
let mut out = Value::Null;
|
||||
for id in &ids {
|
||||
let Some(it) = d.item(*id) else { continue };
|
||||
let cur_name = it.fill.swatch.clone();
|
||||
let cur_name = if stroke { it.stroke.swatch.clone() } else { it.fill.swatch.clone() };
|
||||
let cur = designcraft_color::swatch::resolve_gradient(&d.swatches, &cur_name).cloned();
|
||||
let mut g = cur.clone().unwrap_or_default();
|
||||
if let Some(k) = kind {
|
||||
@@ -964,16 +969,22 @@ fn apply_gradient(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
};
|
||||
let inv = d.find(*id).map(|l| d.parent_xf(&l)).zip(d.item(*id).map(|i| i.xf)).map(|(pxf, xf)| (pxf * xf).inverse());
|
||||
let Some(it) = d.item_mut(*id) else { continue };
|
||||
it.fill.swatch = name.clone();
|
||||
it.fill.tint = 1.0;
|
||||
// The fill's or the stroke's paint: swatch, tint, angle, vector.
|
||||
let (swatch, tint, g_angle, g_vector) = if stroke {
|
||||
(&mut it.stroke.swatch, &mut it.stroke.tint, &mut it.stroke.gradient_angle, &mut it.stroke.gradient_vector)
|
||||
} else {
|
||||
(&mut it.fill.swatch, &mut it.fill.tint, &mut it.fill.gradient_angle, &mut it.fill.gradient_vector)
|
||||
};
|
||||
*swatch = name.clone();
|
||||
*tint = 1.0;
|
||||
if let Some(a) = angle {
|
||||
it.fill.gradient_angle = Some(a);
|
||||
it.fill.gradient_vector = None;
|
||||
*g_angle = Some(a);
|
||||
*g_vector = None;
|
||||
}
|
||||
if let (Some(a), Some(b), Some(inv)) = (from, to, inv) {
|
||||
let (a, b) = (inv * a, inv * b);
|
||||
if (b - a).hypot() > 1e-6 {
|
||||
it.fill.gradient_vector = Some([a.x, a.y, b.x, b.y]);
|
||||
*g_vector = Some([a.x, a.y, b.x, b.y]);
|
||||
}
|
||||
}
|
||||
if out.is_null() {
|
||||
@@ -1997,6 +2008,97 @@ mod radial_gradient_tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod gradient_stroke_tests {
|
||||
use serde_json::json;
|
||||
|
||||
/// Grey along a 20 pt line from (20, 60) to (180, 60): near its start, middle and end.
|
||||
fn along(img: &designcraft_render::Rendered) -> (u8, u8, u8) {
|
||||
(img.pixel(26, 60)[0], img.pixel(100, 60)[0], img.pixel(174, 60)[0])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_gradient_on_a_stroke_runs_across_the_path_on_screen_and_in_pdf() {
|
||||
let mut s = crate::Session::new();
|
||||
s.execute("file.new", &json!({"width": 200, "height": 120, "facingPages": false})).unwrap();
|
||||
let id = s.execute("line.create", &json!({"a": [20, 60], "b": [180, 60]})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("object.stroke", &json!({"ids": [id], "weight": 20})).unwrap();
|
||||
let r = s
|
||||
.execute(
|
||||
"object.gradient",
|
||||
&json!({"ids": [id], "target": "stroke", "stops": [{"location": 0, "color": "#000000"}, {"location": 100, "color": "#ffffff"}]}),
|
||||
)
|
||||
.unwrap();
|
||||
let d = s.doc().unwrap().doc.clone();
|
||||
let it = d.item(designcraft_doc::ItemId(id)).unwrap();
|
||||
assert_eq!(it.stroke.swatch, r["swatch"].as_str().unwrap());
|
||||
assert_eq!(it.fill.swatch, designcraft_color::swatch::NONE, "the fill is left alone");
|
||||
let mut rr = designcraft_render::Renderer::new();
|
||||
let (a, m, b) = along(&rr.render_page(&d, &s.cache, 0, 1.0, false, &Default::default()).unwrap());
|
||||
assert!(a < 40 && (90..170).contains(&m) && b > 215, "screen: {a} {m} {b}");
|
||||
// The PDF, placed back and drawn: the same.
|
||||
let pdf = designcraft_pdf::export_pdf(&d, &s.cache, &Default::default()).unwrap();
|
||||
let mut p = crate::Session::new();
|
||||
p.execute("file.new", &json!({"width": 200, "height": 120, "facingPages": false})).unwrap();
|
||||
p.execute("file.place", &json!({"base64": crate::cmd::file::base64_encode(&pdf), "name": "stroke.pdf", "x": 0, "y": 0, "width": 200}))
|
||||
.unwrap();
|
||||
let pd = p.doc().unwrap().doc.clone();
|
||||
let (a, m, b) = along(&rr.render_page(&pd, &p.cache, 0, 1.0, false, &Default::default()).unwrap());
|
||||
assert!(a < 40 && (90..170).contains(&m) && b > 215, "pdf: {a} {m} {b}");
|
||||
// The Gradient Swatch tool's drag on the stroke: reversed by dragging right to left.
|
||||
s.execute("object.gradient", &json!({"ids": [id], "target": "stroke", "from": [180, 60], "to": [20, 60]})).unwrap();
|
||||
let d = s.doc().unwrap().doc.clone();
|
||||
let (a, _, b) = along(&rr.render_page(&d, &s.cache, 0, 1.0, false, &Default::default()).unwrap());
|
||||
assert!(a > 215 && b < 40, "dragged: {a} {b}");
|
||||
assert!(s.execute("object.gradient", &json!({"ids": [id], "target": "gap"})).is_err());
|
||||
}
|
||||
|
||||
/// A 20 pt horizontal line with a gradient on its stroke, drawn on screen and from its PDF.
|
||||
fn line_with_stroke_gradient(g: serde_json::Value) -> (designcraft_render::Rendered, designcraft_render::Rendered) {
|
||||
let mut s = crate::Session::new();
|
||||
s.execute("file.new", &json!({"width": 200, "height": 120, "facingPages": false})).unwrap();
|
||||
let id = s.execute("line.create", &json!({"a": [20, 60], "b": [180, 60]})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("object.stroke", &json!({"ids": [id], "weight": 20})).unwrap();
|
||||
let mut g = g;
|
||||
g["ids"] = json!([id]);
|
||||
g["target"] = json!("stroke");
|
||||
s.execute("object.gradient", &g).unwrap();
|
||||
let d = s.doc().unwrap().doc.clone();
|
||||
let mut rr = designcraft_render::Renderer::new();
|
||||
let screen = rr.render_page(&d, &s.cache, 0, 1.0, false, &Default::default()).unwrap();
|
||||
let pdf = designcraft_pdf::export_pdf(&d, &s.cache, &Default::default()).unwrap();
|
||||
let mut p = crate::Session::new();
|
||||
p.execute("file.new", &json!({"width": 200, "height": 120, "facingPages": false})).unwrap();
|
||||
p.execute("file.place", &json!({"base64": crate::cmd::file::base64_encode(&pdf), "name": "line.pdf", "x": 0, "y": 0, "width": 200})).unwrap();
|
||||
let pd = p.doc().unwrap().doc.clone();
|
||||
let placed = rr.render_page(&pd, &p.cache, 0, 1.0, false, &Default::default()).unwrap();
|
||||
(screen, placed)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_radial_gradient_on_a_straight_lines_stroke_shows_all_its_stops() {
|
||||
// The line's box has no height: the gradient is a circle across the line's length.
|
||||
let (screen, pdf) = line_with_stroke_gradient(
|
||||
json!({"kind": "radial", "stops": [{"location": 0, "color": "#000000"}, {"location": 100, "color": "#ffffff"}]}),
|
||||
);
|
||||
for (what, img) in [("screen", &screen), ("pdf", &pdf)] {
|
||||
let (a, m, b) = along(img);
|
||||
assert!(m < 40 && a > 180 && b > 180, "{what}: {a} {m} {b}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_linear_gradient_across_a_straight_line_paints_its_stroke_alike_on_screen_and_in_pdf() {
|
||||
let (screen, pdf) =
|
||||
line_with_stroke_gradient(json!({"angle": 90, "stops": [{"location": 0, "color": "#ff0000"}, {"location": 100, "color": "#0000ff"}]}));
|
||||
let (s, p) = (screen.pixel(100, 60), pdf.pixel(100, 60));
|
||||
// The line's box has no height: the gradient runs across the line's length, so the stroke
|
||||
// shows its middle.
|
||||
assert!(s[1] < 60 && s[0] > 80 && s[2] > 80, "screen: the stroke is painted, {s:?}");
|
||||
assert!((0..3).all(|i| s[i].abs_diff(p[i]) < 40), "screen {s:?}, pdf {p:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "text_color_tests.rs"]
|
||||
mod text_color_tests;
|
||||
|
||||
@@ -1776,6 +1776,14 @@ impl<'a> Ex<'a> {
|
||||
} else if let Some(a) = it.fill.gradient_angle {
|
||||
el.set("GradientFillAngle", num(a));
|
||||
}
|
||||
// The same on the stroke.
|
||||
if let Some([x0, y0, x1, y1]) = it.stroke.gradient_vector {
|
||||
el.set("GradientStrokeStart", format!("{} {}", num(x0), num(y0)));
|
||||
el.set("GradientStrokeLength", num((x1 - x0).hypot(y1 - y0)));
|
||||
el.set("GradientStrokeAngle", num((-(y1 - y0)).atan2(x1 - x0).to_degrees()));
|
||||
} else if let Some(a) = it.stroke.gradient_angle {
|
||||
el.set("GradientStrokeAngle", num(a));
|
||||
}
|
||||
self.stroke_attrs(&mut el, &it.stroke);
|
||||
// Corner options: map path-order corners to the named corners by geometry.
|
||||
if !it.corners.is_none() {
|
||||
|
||||
@@ -2347,6 +2347,19 @@ impl<'r> Importer<'r> {
|
||||
it.stroke.swatch = swatch::NONE.into();
|
||||
it.stroke.weight = 1.0;
|
||||
}
|
||||
// A gradient on the stroke: its angle, or the Gradient Swatch tool's vector.
|
||||
if let Some(a) = e.num("GradientStrokeAngle")
|
||||
&& a != 0.0
|
||||
{
|
||||
it.stroke.gradient_angle = Some(a);
|
||||
}
|
||||
let start: Option<Vec<f64>> = e.prop("GradientStrokeStart").map(|s| s.split_whitespace().filter_map(|v| v.parse().ok()).collect());
|
||||
if let (Some([x, y]), Some(len)) = (start.as_deref().and_then(|v| <[f64; 2]>::try_from(v).ok()), e.num("GradientStrokeLength"))
|
||||
&& len > 0.0
|
||||
{
|
||||
let a = e.num("GradientStrokeAngle").unwrap_or(0.0).to_radians();
|
||||
it.stroke.gradient_vector = Some([x, y, x + len * a.cos(), y - len * a.sin()]);
|
||||
}
|
||||
}
|
||||
// Corners: each corner's shape and radius come from the item, then its object style chain;
|
||||
// at each level a per-corner attribute wins over the legacy all-corners one.
|
||||
|
||||
@@ -2176,6 +2176,23 @@ fn type_on_a_path_keeps_its_start_and_end_brackets() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_strokes_gradient_vector_round_trips() {
|
||||
let mut d = designcraft_doc::Document::new(&designcraft_doc::build::NewDocument::default());
|
||||
let id = designcraft_doc::ItemId(d.alloc());
|
||||
let mut it = designcraft_doc::Item::new(
|
||||
id,
|
||||
d.default_layer(),
|
||||
designcraft_doc::Shape::Path,
|
||||
designcraft_geom::shapes::rectangle(designcraft_geom::Rect::new(10.0, 10.0, 110.0, 60.0)),
|
||||
);
|
||||
it.stroke.gradient_vector = Some([10.0, 35.0, 110.0, 35.0]);
|
||||
d.insert_item(designcraft_doc::SpreadRef::Doc(0), it, None).unwrap();
|
||||
let back = crate::import_idml(&crate::export_idml(&d)).unwrap();
|
||||
let v = back.spreads[0].items[0].stroke.gradient_vector.unwrap();
|
||||
assert!(v.iter().zip([10.0, 35.0, 110.0, 35.0]).all(|(a, b)| (a - b).abs() < 1e-6), "{v:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frame_preference_import_caps_column_count_without_composition() {
|
||||
for (raw, expected) in [("4000000000", 216), ("1e300", 216), ("-100", 1), ("0", 1), ("NaN", 1), ("inf", 1), ("-inf", 1)] {
|
||||
|
||||
+14
-15
@@ -989,7 +989,7 @@ impl Exporter<'_> {
|
||||
|
||||
/// A swatch fill (solid or gradient) in the item's inner space. `None` for [None]/unknown.
|
||||
fn fill_paint(&mut self, fill: &designcraft_doc::Fill, bounds: Rect) -> Option<krilla::paint::Paint> {
|
||||
let (swatch, tint, angle) = (fill.swatch.as_str(), fill.tint, fill.gradient_angle);
|
||||
let (swatch, tint) = (fill.swatch.as_str(), fill.tint);
|
||||
if let Some(g) = designcraft_color::swatch::resolve_gradient(&self.doc.swatches, swatch) {
|
||||
let kind = g.kind;
|
||||
let mut expanded = g.expanded_stops();
|
||||
@@ -1030,8 +1030,6 @@ impl Exporter<'_> {
|
||||
if stops.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let c = bounds.center();
|
||||
let v = fill.gradient_vector;
|
||||
return Some(match kind {
|
||||
GradientKind::Radial => {
|
||||
// A circle stretched into the object's ellipse, as on screen.
|
||||
@@ -1054,13 +1052,7 @@ impl Exporter<'_> {
|
||||
.into()
|
||||
}
|
||||
_ => {
|
||||
let a = angle.unwrap_or(0.0).to_radians();
|
||||
let half = ((bounds.width() * a.cos().abs() + bounds.height() * a.sin().abs()) / 2.0).max(1e-3);
|
||||
let d = Vec2::new(a.cos(), -a.sin()) * half;
|
||||
let (p0, p1) = match v {
|
||||
Some([x0, y0, x1, y1]) => (designcraft_geom::Point::new(x0, y0), designcraft_geom::Point::new(x1, y1)),
|
||||
None => (c - d, c + d),
|
||||
};
|
||||
let (p0, p1) = fill.linear_axis(bounds);
|
||||
LinearGradient {
|
||||
x1: p0.x as f32,
|
||||
y1: p0.y as f32,
|
||||
@@ -1080,7 +1072,14 @@ impl Exporter<'_> {
|
||||
|
||||
fn stroke(&mut self, s: &mut Surface, it: &Item, bp: &BezPath, path: &Path) {
|
||||
let st = &it.stroke;
|
||||
let Some(c) = self.swatch_color(&st.swatch, st.tint) else { return };
|
||||
// A gradient swatch runs across the path's box, as on a fill.
|
||||
let paint: krilla::paint::Paint = if designcraft_color::swatch::resolve_gradient(&self.doc.swatches, &st.swatch).is_some() {
|
||||
let Some(p) = self.fill_paint(&st.as_fill(), bp.bounding_box()) else { return };
|
||||
p
|
||||
} else {
|
||||
let Some(c) = self.swatch_color(&st.swatch, st.tint) else { return };
|
||||
c.into()
|
||||
};
|
||||
let closed = it.path.is_closed();
|
||||
let mut cap = match st.cap {
|
||||
designcraft_doc::Cap::Butt => krilla::paint::LineCap::Butt,
|
||||
@@ -1101,7 +1100,7 @@ impl Exporter<'_> {
|
||||
s.draw_path(path);
|
||||
s.set_stroke(None);
|
||||
}
|
||||
s.set_fill(Some(Fill { paint: c.into(), opacity: NormalizedF32::ONE, rule: FillRule::NonZero }));
|
||||
s.set_fill(Some(Fill { paint: paint.clone(), opacity: NormalizedF32::ONE, rule: FillRule::NonZero }));
|
||||
s.draw_path(&op);
|
||||
s.set_fill(None);
|
||||
return;
|
||||
@@ -1153,7 +1152,7 @@ impl Exporter<'_> {
|
||||
s.draw_path(path);
|
||||
}
|
||||
s.set_stroke(Some(Stroke {
|
||||
paint: c.clone().into(),
|
||||
paint: paint.clone(),
|
||||
width: width as f32,
|
||||
miter_limit: st.miter_limit.max(1.0) as f32,
|
||||
line_cap: cap,
|
||||
@@ -1174,12 +1173,12 @@ impl Exporter<'_> {
|
||||
let Some(p) = to_path(&h.path) else { continue };
|
||||
match h.outline {
|
||||
Some(w) => {
|
||||
s.set_stroke(Some(Stroke { paint: c.clone().into(), width: w as f32, opacity: NormalizedF32::ONE, ..Default::default() }));
|
||||
s.set_stroke(Some(Stroke { paint: paint.clone(), width: w as f32, opacity: NormalizedF32::ONE, ..Default::default() }));
|
||||
s.draw_path(&p);
|
||||
s.set_stroke(None);
|
||||
}
|
||||
None => {
|
||||
s.set_fill(Some(krilla::paint::Fill { paint: c.clone().into(), opacity: NormalizedF32::ONE, rule: FillRule::NonZero }));
|
||||
s.set_fill(Some(krilla::paint::Fill { paint: paint.clone(), opacity: NormalizedF32::ONE, rule: FillRule::NonZero }));
|
||||
s.draw_path(&p);
|
||||
s.set_fill(None);
|
||||
}
|
||||
|
||||
+17
-10
@@ -620,8 +620,17 @@ impl Renderer {
|
||||
}
|
||||
|
||||
fn draw_stroke(&mut self, ctx: &mut RenderContext, f: &Frame, it: &Item, bp: &BezPath, xf: Affine) {
|
||||
self.draw_stroke_painted(ctx, f, it, bp, xf);
|
||||
ctx.reset_paint_transform();
|
||||
}
|
||||
|
||||
/// The stroke, in its colour or gradient (a gradient runs across the path's box, as a fill's).
|
||||
fn draw_stroke_painted(&mut self, ctx: &mut RenderContext, f: &Frame, it: &Item, bp: &BezPath, xf: Affine) {
|
||||
let st = &it.stroke;
|
||||
let Some(c) = f.doc.resolve_color(&st.swatch, st.tint) else { return };
|
||||
let (paint, bounds) = (st.as_fill(), bp.bounding_box());
|
||||
if f.doc.resolve_color(&st.swatch, st.tint).is_none() && designcraft_color::swatch::resolve_gradient(&f.doc.swatches, &st.swatch).is_none() {
|
||||
return;
|
||||
}
|
||||
let mut stroke = kurbo::Stroke::new(st.weight)
|
||||
.with_caps(match st.cap {
|
||||
designcraft_doc::Cap::Butt => kurbo::Cap::Butt,
|
||||
@@ -641,7 +650,9 @@ impl Renderer {
|
||||
_ => {}
|
||||
}
|
||||
ctx.set_transform(f.view * xf);
|
||||
ctx.set_paint(color_of(&c, 1.0));
|
||||
if !set_fill_paint(ctx, f.doc, &paint, bounds) {
|
||||
return;
|
||||
}
|
||||
let closed = it.path.is_closed();
|
||||
// Arrowheads: the path is shortened under them; they're drawn after it.
|
||||
let arrows = designcraft_doc::arrow::apply(bp, st, closed);
|
||||
@@ -668,7 +679,7 @@ impl Renderer {
|
||||
if op {
|
||||
ctx.pop_layer();
|
||||
}
|
||||
ctx.set_paint(color_of(&c, 1.0));
|
||||
set_fill_paint(ctx, f.doc, &paint, bounds);
|
||||
}
|
||||
// Stripes, wavy and hash strokes are fills (along the path's centre line).
|
||||
if let Some(o) = kind.outline(bp, st.weight, 0.05 * f.px) {
|
||||
@@ -877,7 +888,7 @@ pub fn color_of(c: &designcraft_color::Color, alpha: f32) -> peniko::Color {
|
||||
|
||||
/// Set a swatch fill (solid or gradient) as the paint. Returns false for [None]/unknown.
|
||||
fn set_fill_paint(ctx: &mut RenderContext, doc: &Document, fill: &designcraft_doc::Fill, bounds: Rect) -> bool {
|
||||
let (swatch, tint, angle) = (fill.swatch.as_str(), fill.tint, fill.gradient_angle);
|
||||
let (swatch, tint) = (fill.swatch.as_str(), fill.tint);
|
||||
if let Some(g) = designcraft_color::swatch::resolve_gradient(&doc.swatches, swatch) {
|
||||
// Sampled so process colours blend as inks, as InDesign shows them (and as the PDF's CMYK
|
||||
// shading prints).
|
||||
@@ -885,7 +896,6 @@ fn set_fill_paint(ctx: &mut RenderContext, doc: &Document, fill: &designcraft_do
|
||||
if stops.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let c = bounds.center();
|
||||
let grad = match (g.kind, fill.gradient_vector) {
|
||||
// Radial: a circle stretched into the object's ellipse (the paint transform).
|
||||
(designcraft_color::GradientKind::Radial, _) => {
|
||||
@@ -894,12 +904,9 @@ fn set_fill_paint(ctx: &mut RenderContext, doc: &Document, fill: &designcraft_do
|
||||
ctx.set_paint_transform(Affine::translate(c.to_vec2()) * Affine::scale_non_uniform(1.0, ry / rx) * Affine::translate(-c.to_vec2()));
|
||||
peniko::Gradient::new_radial(c, rx as f32).with_stops(stops.as_slice())
|
||||
}
|
||||
(_, Some([x0, y0, x1, y1])) => peniko::Gradient::new_linear((x0, y0), (x1, y1)).with_stops(stops.as_slice()),
|
||||
_ => {
|
||||
let a = angle.unwrap_or(0.0).to_radians();
|
||||
let half = (bounds.width() * a.cos().abs() + bounds.height() * a.sin().abs()) / 2.0;
|
||||
let d = Vec2::new(a.cos(), -a.sin()) * half;
|
||||
peniko::Gradient::new_linear(c - d, c + d).with_stops(stops.as_slice())
|
||||
let (a, b) = fill.linear_axis(bounds);
|
||||
peniko::Gradient::new_linear(a, b).with_stops(stops.as_slice())
|
||||
}
|
||||
};
|
||||
ctx.set_paint(grad);
|
||||
|
||||
@@ -74,6 +74,16 @@ const TABLE: &[(&str, [&str; 5])] = &[
|
||||
("Frame Edges", ["Rahmenkanten", "Bords du bloc", "Bordes del marco", "フレーム枠", "框架边缘"]),
|
||||
("Rulers", ["Lineale", "Règles", "Reglas", "定規", "标尺"]),
|
||||
("Hidden Characters", ["Verborgene Zeichen", "Caractères masqués", "Caracteres ocultos", "制御文字", "隐藏字符"]),
|
||||
(
|
||||
"The stroke isn't a gradient.",
|
||||
[
|
||||
"Die Kontur ist kein Verlauf.",
|
||||
"Le contour n’est pas un dégradé.",
|
||||
"El trazo no es un degradado.",
|
||||
"線はグラデーションではありません。",
|
||||
"描边不是渐变。",
|
||||
],
|
||||
),
|
||||
("Reverse Path", ["Pfadrichtung umkehren", "Inverser le tracé", "Invertir trazado", "パスの方向反転", "反转路径"]),
|
||||
("Set Anchor", ["Ankerpunkt festlegen", "Définir le point d’ancrage", "Definir punto de ancla", "アンカーポイントを設定", "设置锚点"]),
|
||||
("Clear", ["Löschen", "Effacer", "Borrar", "消去", "清除"]),
|
||||
|
||||
@@ -37,6 +37,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("View Options", "خيارات العرض"),
|
||||
("Frame Edges", "حواف الإطار"),
|
||||
("Hidden Characters", "الأحرف المخفية"),
|
||||
("The stroke isn't a gradient.", "الحد ليس تدرجًا."),
|
||||
("Reverse Path", "عكس المسار"),
|
||||
("Set Anchor", "تعيين نقطة الارتساء"),
|
||||
("Clear", "مسح"),
|
||||
|
||||
@@ -35,6 +35,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("View Options", "Opzioni di visualizzazione"),
|
||||
("Frame Edges", "Bordi cornice"),
|
||||
("Hidden Characters", "Caratteri nascosti"),
|
||||
("The stroke isn't a gradient.", "La traccia non è una sfumatura."),
|
||||
("Reverse Path", "Inverti tracciato"),
|
||||
("Set Anchor", "Imposta ancoraggio"),
|
||||
("Clear", "Cancella"),
|
||||
|
||||
@@ -36,6 +36,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("View Options", "表示オプション"),
|
||||
("Frame Edges", "フレーム枠"),
|
||||
("Hidden Characters", "制御文字"),
|
||||
("The stroke isn't a gradient.", "線はグラデーションではありません。"),
|
||||
("Reverse Path", "パスの方向反転"),
|
||||
("Set Anchor", "アンカーポイントを設定"),
|
||||
("Clear", "消去"),
|
||||
|
||||
@@ -41,6 +41,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("View Options", "Opções de visualização"),
|
||||
("Frame Edges", "Bordas do quadro"),
|
||||
("Hidden Characters", "Caracteres ocultos"),
|
||||
("The stroke isn't a gradient.", "O traçado não é um gradiente."),
|
||||
("Reverse Path", "Inverter demarcador"),
|
||||
("Set Anchor", "Definir ponto de ancoragem"),
|
||||
("Clear", "Limpar"),
|
||||
|
||||
@@ -38,6 +38,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("View Options", "Параметри перегляду"),
|
||||
("Frame Edges", "Краї фреймів"),
|
||||
("Hidden Characters", "Приховані символи"),
|
||||
("The stroke isn't a gradient.", "Обведення не є градієнтом."),
|
||||
("Reverse Path", "Розвернути контур"),
|
||||
("Set Anchor", "Задати прив’язку"),
|
||||
("Clear", "Очистити"),
|
||||
|
||||
@@ -520,11 +520,21 @@ pub fn gradient_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
));
|
||||
return;
|
||||
};
|
||||
let fill = it.fill.clone();
|
||||
// The fill's gradient, or the stroke's when the toolbox's Stroke box is in front (as InDesign's
|
||||
// Gradient panel follows the Fill/Stroke proxy).
|
||||
let target = if app.ui.stroke_front { "stroke" } else { "fill" };
|
||||
let fill = if app.ui.stroke_front { it.stroke.as_fill() } else { it.fill.clone() };
|
||||
let Some(g) = designcraft_color::swatch::resolve_gradient(&st.doc.swatches, &fill.swatch).cloned() else {
|
||||
crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(&app.ui.language, "The fill isn't a gradient.")).color(t.text_dim));
|
||||
crate::rtl::label(
|
||||
ui,
|
||||
egui::RichText::new(crate::i18n::tr(
|
||||
&app.ui.language,
|
||||
if app.ui.stroke_front { "The stroke isn't a gradient." } else { "The fill isn't a gradient." },
|
||||
))
|
||||
.color(t.text_dim),
|
||||
);
|
||||
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Apply Gradient"))).clicked() {
|
||||
let _ = app.run("object.gradient", json!({}));
|
||||
let _ = app.run("object.gradient", json!({"target": target, "target": target}));
|
||||
}
|
||||
return;
|
||||
};
|
||||
@@ -537,7 +547,7 @@ pub fn gradient_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
.show_ui(ui, |ui| {
|
||||
for (k, v) in [("Linear", "linear"), ("Radial", "radial")] {
|
||||
if ui.selectable_label(g.kind.label() == k, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, k))).clicked() {
|
||||
let _ = app.run("object.gradient", json!({"kind": v}));
|
||||
let _ = app.run("object.gradient", json!({"target": target, "kind": v}));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -548,7 +558,7 @@ pub fn gradient_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
})
|
||||
.clicked()
|
||||
{
|
||||
let _ = app.run("object.gradient", json!({"reverse": true}));
|
||||
let _ = app.run("object.gradient", json!({"target": target, "reverse": true}));
|
||||
}
|
||||
});
|
||||
let sel_id = egui::Id::new("grad_sel");
|
||||
@@ -560,13 +570,13 @@ pub fn gradient_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
if let Some(v) = crate::widgets::number(ui, "grad_loc", loc, " %", 54.0, 1) {
|
||||
let mut stops = stops_json(&g);
|
||||
stops[sel]["location"] = json!(v.clamp(0.0, 100.0));
|
||||
let _ = app.run("object.gradient", json!({"stops": stops}));
|
||||
let _ = app.run("object.gradient", json!({"target": target, "stops": stops}));
|
||||
}
|
||||
if !radial {
|
||||
crate::widgets::caption(ui, crate::i18n::tr(&app.ui.language, "Angle:"));
|
||||
let angle = fill.gradient_vector.map(|[x0, y0, x1, y1]| (-(y1 - y0)).atan2(x1 - x0).to_degrees()).or(fill.gradient_angle).unwrap_or(0.0);
|
||||
if let Some(v) = crate::widgets::number(ui, "grad_angle", Some(angle), "°", 54.0, 1) {
|
||||
let _ = app.run("object.gradient", json!({"angle": v}));
|
||||
let _ = app.run("object.gradient", json!({"target": target, "angle": v}));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -616,7 +626,7 @@ pub fn gradient_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
} else {
|
||||
stops[i]["location"] = json!(o as f64 * 100.0);
|
||||
}
|
||||
let _ = app.run("object.gradient", json!({"stops": stops}));
|
||||
let _ = app.run("object.gradient", json!({"target": target, "stops": stops}));
|
||||
}
|
||||
if resp.clicked()
|
||||
&& let Some(p) = resp.interact_pointer_pos()
|
||||
@@ -629,7 +639,7 @@ pub fn gradient_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
let (c, a) = g.sample(o);
|
||||
let mut stops = stops_json(&g);
|
||||
stops.push(json!({"location": o as f64 * 100.0, "color": c.to_hex(), "opacity": a as f64 * 100.0, "midpoint": 50.0}));
|
||||
let _ = app.run("object.gradient", json!({"stops": stops}));
|
||||
let _ = app.run("object.gradient", json!({"target": target, "stops": stops}));
|
||||
sel = g.stops.iter().filter(|s| s.offset <= o).count();
|
||||
}
|
||||
None => {}
|
||||
@@ -678,7 +688,7 @@ pub fn gradient_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
if egui::color_picker::color_edit_button_srgb(ui, &mut rgb).changed() {
|
||||
let mut stops = stops_json(&g);
|
||||
stops[sel]["color"] = json!(format!("#{:02x}{:02x}{:02x}", rgb[0], rgb[1], rgb[2]));
|
||||
let _ = app.run("object.gradient", json!({"stops": stops}));
|
||||
let _ = app.run("object.gradient", json!({"target": target, "stops": stops}));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -424,7 +424,7 @@ It is evidence of breadth: each item exists. It is not a claim of InDesign-level
|
||||
- View › Display Performance: Fast (grey boxes, no effects, ⌥⇧⌘Z), Typical (72 ppi proxies, ⌥⌘Z), High Quality (⌥⌘H, our default — the renderer is fast enough).
|
||||
- OpenType menu (Character panel): discretionary ligatures, fractions, ordinal, swash, titling, contextual alternates, slashed zero, the four figure styles and stylistic sets 1–20; `type.openType`; mapped to IDML's OTF attributes.
|
||||
- Add Anchor Point (=), Delete Anchor Point (-) and Convert Direction Point (⇧C) tools (click toggles smooth/corner, drag pulls out handles), with `path.addAnchor`, `path.deleteAnchor`, `path.convertAnchor`.
|
||||
- Gradients: the Gradient Swatch tool (G) drags a gradient's start and end across objects (Shift: 45°); the Gradient panel sets type, angle, reverse and edits stops on a ramp (drag, add, drag off to remove, location, colour). Edited gradients are unnamed swatches; `object.gradient`; the vector round-trips through IDML. A radial gradient stretches with the object's box as InDesign's does (an ellipse reaching the sides of a non-square frame, on screen and in PDF).
|
||||
- Gradients: the Gradient Swatch tool (G) drags a gradient's start and end across objects (Shift: 45°); the Gradient panel sets type, angle, reverse and edits stops on a ramp (drag, add, drag off to remove, location, colour). Edited gradients are unnamed swatches; `object.gradient` (`target: stroke` for a stroke's gradient, drawn across the path's box on screen and in PDF); the vector round-trips through IDML (`GradientFill…`/`GradientStroke…`). The Gradient panel follows the Fill/Stroke proxy. A radial gradient stretches with the object's box as InDesign's does (an ellipse reaching the sides of a non-square frame, on screen and in PDF).
|
||||
- Document Setup (⌥⌘P): intent, number of pages, start page # (an even start begins with a left page), facing pages, size presets and orientation, bleed and slug per edge; `layout.documentSetup {}` reports the setup.
|
||||
- Stroke panel: cap, join, miter limit, alignment, type, Start/End arrowheads (12 kinds, drawn on screen and in PDF; the path is shortened under pointed heads) and gap colour under dashes and dots.
|
||||
- Preferences (⌘K: General, Type, Units & Increments, Grids, Guides & Pasteboard, Display Performance) backed by `prefs.set` (application) and `document.preferences` (units, increments, grids — undoable, saved with the document; right-click a ruler to pick its units, the horizontal and vertical rulers each have their own); scaling applies to content and scales stroke weights (Include Stroke Weight), and X/Y/W/H measure the stroke's outer edge (Dimensions Include Stroke Weight). Quick Apply (⌘Return) finds paragraph, character and object styles as well as commands.
|
||||
|
||||
Reference in New Issue
Block a user