mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 10:47:27 +00:00
Merge pull request #6018 from mockydevs/fix/eps-sampled-function-shading
EPS import: sampled functions interpolate between their samples (function shadings no longer come in as flat blocks)
This commit is contained in:
@@ -292,7 +292,9 @@ impl Interp<'_> {
|
||||
// there are too many).
|
||||
let mut visits = 0;
|
||||
let points = linear_points(&f, 0, &mut visits).filter(|p| p.len() <= MAX_STITCH_STOPS);
|
||||
let stitched = points.is_some();
|
||||
// A sampled function is continuous: no hard colour changes at its samples.
|
||||
let stitched =
|
||||
points.is_some() && !matches!(&f, Obj::Dict(d) if d.borrow().get(&Key::name("FunctionType")).and_then(Obj::as_num) == Some(0.0));
|
||||
let ts: Vec<f64> = match points {
|
||||
Some(mut pts) => {
|
||||
pts.retain(|t| (d0.min(d1)..=d0.max(d1)).contains(t));
|
||||
@@ -452,6 +454,23 @@ fn linear_points(f: &Obj, depth: u32, visits: &mut usize) -> Option<Vec<f64>> {
|
||||
let num = |k: &str| d.get(&Key::name(k)).and_then(Obj::as_num);
|
||||
match num("FunctionType")? {
|
||||
2.0 if num("N").unwrap_or(1.0) == 1.0 => Some(vec![]),
|
||||
// A sampled function of one input, interpolated linearly: straight between its samples.
|
||||
0.0 if depth == 0 && num("Order").unwrap_or(1.0) == 1.0 => {
|
||||
let nums = |k: &str| -> Vec<f64> {
|
||||
d.get(&Key::name(k)).and_then(Obj::items).map(|i| i.borrow().iter().filter_map(Obj::as_num).collect()).unwrap_or_default()
|
||||
};
|
||||
let (domain, size, encode) = (nums("Domain"), nums("Size"), nums("Encode"));
|
||||
let ([a, b], [s]) = (<[f64; 2]>::try_from(domain).ok()?, <[f64; 1]>::try_from(size).ok()?);
|
||||
if !(1.0..=MAX_STITCH_STOPS as f64).contains(&s) || !a.is_finite() || !b.is_finite() {
|
||||
return None;
|
||||
}
|
||||
let (e0, e1) = (encode.first().copied().unwrap_or(0.0), encode.get(1).copied().unwrap_or(s - 1.0));
|
||||
if e1 == e0 || !e0.is_finite() || !e1.is_finite() {
|
||||
return None;
|
||||
}
|
||||
// Where each sample index `k` falls in the domain, through `Encode`.
|
||||
Some((0..s as usize).map(|k| a + (k as f64 - e0) * (b - a) / (e1 - e0)).filter(|x| x.is_finite()).collect())
|
||||
}
|
||||
3.0 => {
|
||||
let items = d.get(&Key::name("Functions"))?.items()?;
|
||||
// More pieces than the stop cap allows: sample the function instead (tested before
|
||||
|
||||
@@ -22,6 +22,9 @@ const FUNCTION_GRID: u32 = 16;
|
||||
const MAX_SAMPLES: usize = 1 << 24;
|
||||
/// Most outputs (colour components) a sampled function has.
|
||||
const MAX_OUTPUTS: usize = 32;
|
||||
/// Most samples one value of a sampled function is interpolated from: inputs past it take the
|
||||
/// nearest sample.
|
||||
const MAX_TAPS: usize = 256;
|
||||
/// Work one patch costs against the operation budget.
|
||||
const PATCH_OPS: u64 = 64;
|
||||
|
||||
@@ -312,8 +315,8 @@ impl Interp<'_> {
|
||||
Ok(Some(out))
|
||||
}
|
||||
|
||||
/// A sampled function (type 0) at `x`: the samples around it, interpolated linearly for
|
||||
/// one input, the nearest one for more.
|
||||
/// A sampled function (type 0) at `x`: the samples around it, interpolated (multilinearly,
|
||||
/// or by cubics for `Order 3`).
|
||||
pub(super) fn sampled(&mut self, f: &DictRef, x: &[f64]) -> Res<Vec<f64>> {
|
||||
let get = |k: &str| f.borrow().get(&Key::name(k)).cloned();
|
||||
let nums =
|
||||
@@ -335,6 +338,8 @@ impl Interp<'_> {
|
||||
Some(Obj::File { stream, .. }) => self.peek_all(&stream)?,
|
||||
_ => return ps_err("typecheck", "DataSource"),
|
||||
};
|
||||
// Copying the samples is work too: a shading evaluates its function once for each stop.
|
||||
self.spend((bytes.len() / 256) as u64)?;
|
||||
let (encode, decode) = (nums("Encode"), nums("Decode"));
|
||||
let max = ((1u64 << bps) - 1) as f64;
|
||||
let sample = |index: usize, j: usize| -> f64 {
|
||||
@@ -360,17 +365,68 @@ impl Interp<'_> {
|
||||
let e = if b != a { e0 + (xi - a) * (e1 - e0) / (b - a) } else { e0 };
|
||||
pos.push(clamp_between(e, 0.0, s - 1.0));
|
||||
}
|
||||
if let ([p], Some(s)) = (pos.as_slice(), size.first()) {
|
||||
let (lo, hi) = (p.floor() as usize, (p.ceil() as usize).min(*s as usize - 1));
|
||||
let t = p - p.floor();
|
||||
return Ok((0..outs).map(|j| sample(lo, j) * (1.0 - t) + sample(hi, j) * t).collect());
|
||||
}
|
||||
let mut index = 0;
|
||||
let mut stride = 1;
|
||||
// The samples each input reads and their weights: the two around it (linear), or the
|
||||
// four around it for `Order 3` (cubic). The result is their tensor product.
|
||||
let cubic = get("Order").and_then(|o| o.as_num()) == Some(3.0);
|
||||
let mut taps: Vec<Vec<(usize, f64)>> = Vec::with_capacity(m);
|
||||
let mut combos = 1usize;
|
||||
for (i, p) in pos.iter().enumerate() {
|
||||
index += p.round() as usize * stride;
|
||||
stride *= size.get(i).copied().unwrap_or(1.0) as usize;
|
||||
let last = size.get(i).copied().unwrap_or(1.0) as usize - 1;
|
||||
let lo = p.floor() as usize;
|
||||
let t = p - p.floor();
|
||||
let at = |k: isize| (lo as isize + k).clamp(0, last as isize) as usize;
|
||||
let w: Vec<(usize, f64)> = if t == 0.0 || last == 0 {
|
||||
vec![(lo.min(last), 1.0)]
|
||||
} else if cubic && combos.saturating_mul(4) <= MAX_TAPS {
|
||||
catmull_rom(t).into_iter().zip(-1..=2).map(|(w, k)| (at(k), w)).collect()
|
||||
} else if combos.saturating_mul(2) <= MAX_TAPS {
|
||||
vec![(at(0), 1.0 - t), (at(1), t)]
|
||||
} else {
|
||||
// Too many inputs between samples to interpolate them all: the nearest (a
|
||||
// fractional `Size` can put it past the last sample).
|
||||
vec![((p.round() as usize).min(last), 1.0)]
|
||||
};
|
||||
combos = combos.saturating_mul(w.len());
|
||||
taps.push(w);
|
||||
}
|
||||
Ok((0..outs).map(|j| sample(index, j)).collect())
|
||||
// Each combination of taps reads one sample of every output.
|
||||
self.spend(combos.saturating_mul(outs) as u64)?;
|
||||
let mut out = vec![0.0; outs];
|
||||
let mut digits = vec![0usize; m];
|
||||
loop {
|
||||
let (mut index, mut stride, mut weight) = (0usize, 1usize, 1.0);
|
||||
for (i, (tap, d)) in taps.iter().zip(&digits).enumerate() {
|
||||
let (k, w) = tap.get(*d).copied().unwrap_or((0, 0.0));
|
||||
index += k * stride;
|
||||
weight *= w;
|
||||
stride *= size.get(i).copied().unwrap_or(1.0) as usize;
|
||||
}
|
||||
if weight != 0.0 {
|
||||
for (j, o) in out.iter_mut().enumerate() {
|
||||
*o += weight * sample(index, j);
|
||||
}
|
||||
}
|
||||
// The next combination, the first input varying fastest.
|
||||
let Some(i) = digits.iter().zip(&taps).position(|(d, t)| d + 1 < t.len()) else { break };
|
||||
for d in digits.iter_mut().take(i) {
|
||||
*d = 0;
|
||||
}
|
||||
if let Some(d) = digits.get_mut(i) {
|
||||
*d += 1;
|
||||
}
|
||||
}
|
||||
// A cubic overshoots between samples of different slopes: kept in the range.
|
||||
Ok(out
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(j, v)| clamp_between(v, range.get(2 * j).copied().unwrap_or(0.0), range.get(2 * j + 1).copied().unwrap_or(1.0)))
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
/// The weights of the four samples around a point `t` of the way from the second to the third:
|
||||
/// the Catmull-Rom cubic, which passes through every sample with the slope of its neighbours.
|
||||
fn catmull_rom(t: f64) -> [f64; 4] {
|
||||
let (t2, t3) = (t * t, t * t * t);
|
||||
[(-t3 + 2.0 * t2 - t) / 2.0, (3.0 * t3 - 5.0 * t2 + 2.0) / 2.0, (-3.0 * t3 + 4.0 * t2 + t) / 2.0, (t3 - t2) / 2.0]
|
||||
}
|
||||
|
||||
@@ -677,6 +677,10 @@ fn mesh_and_function_shadings_become_gradient_meshes() {
|
||||
let NodeKind::Mesh(g) = &m.kind else { panic!() };
|
||||
// Its first corner (the domain's origin) is the first sample.
|
||||
assert_eq!(g.points[0].color, Color::rgb(1.0, 0.0, 0.0));
|
||||
// Between the samples it is interpolated (PLRM 3rd ed. §3.10.1, type 0 functions), not the
|
||||
// nearest sample: the middle of the 17 × 17 grid is the average of red, green, blue and black.
|
||||
let mid = g.points[8 * 17 + 8].color.to_rgb_uncalibrated();
|
||||
assert!(mid.iter().all(|c| (c - 0.25).abs() < 1e-3), "{mid:?}");
|
||||
|
||||
// Type 4 packed: 8-bit flags, coordinates and components; a triangle and one on its edge.
|
||||
let tri = [[0u8, 0, 0, 255, 0, 0], [0, 255, 0, 0, 255, 0], [0, 0, 255, 0, 0, 255], [1, 255, 255, 255, 255, 0]].concat();
|
||||
@@ -1141,3 +1145,31 @@ fn filter_output_counts_against_the_memory_budget() {
|
||||
let Err(err) = many else { panic!("the budget ends the program") };
|
||||
assert!(err.contains("too much memory"), "{err}");
|
||||
}
|
||||
|
||||
/// A sampled function of one input in an axial shading: interpolated linearly, its gradient has a
|
||||
/// stop at every sample (exact, not sampled evenly); with `Order 3` it is a cubic through the
|
||||
/// samples (PLRM 3rd ed. §3.10.1, Table 3.36), kept in its range.
|
||||
#[test]
|
||||
fn sampled_functions_interpolate_linearly_or_by_cubics() {
|
||||
let shading = |order: &str| {
|
||||
let r = read(&format!(
|
||||
"<< /ShadingType 2 /ColorSpace /DeviceGray /Coords [0 0 100 0] /Function << /FunctionType 0 /Domain [0 1] \
|
||||
/Range [0 1] /Size [4] /BitsPerSample 8 {order} /DataSource <00ffff00> >> >> shfill"
|
||||
));
|
||||
let Some(Paint::Gradient(g)) = all(&r.document).into_iter().find_map(|n| fill(&n).cloned()) else { panic!("no gradient") };
|
||||
g.gradient.stops.iter().map(|s| (s.offset, s.color.to_rgb_uncalibrated()[0])).collect::<Vec<_>>()
|
||||
};
|
||||
let linear = shading("");
|
||||
assert_eq!(linear.len(), 4, "{linear:?}");
|
||||
for ((t, v), (et, ev)) in linear.iter().zip([(0.0, 0.0), (1.0 / 3.0, 1.0), (2.0 / 3.0, 1.0), (1.0, 0.0)]) {
|
||||
assert!((t - et).abs() < 1e-4 && (v - ev).abs() < 1e-3, "{linear:?}");
|
||||
}
|
||||
// Sampled evenly; at 3/32 (0.28 of the way from the first sample to the second) the cubic
|
||||
// through 0, 0, 1 and 1 (the first sample repeated before it) is 0.237, where a straight line
|
||||
// would be 0.281. Between the two white samples it would overshoot past 1, and is kept at 1.
|
||||
let cubic = shading("/Order 3");
|
||||
let at = |t: f32| cubic.iter().find(|s| (s.0 - t).abs() < 1e-4).map(|s| s.1);
|
||||
assert!(at(3.0 / 32.0).is_some_and(|v| (v - 0.237).abs() < 0.01), "{cubic:?}");
|
||||
assert!(cubic.iter().all(|s| (0.0..=1.0).contains(&s.1)), "{cubic:?}");
|
||||
assert!(cubic.iter().any(|s| s.1 == 1.0), "{cubic:?}");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# File format parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-07 · **Change:** minor (`.ai` and EPS type from the text document keeps its tab stops; it keeps auto leading and hyphenation options, and may show more of the file's last lines in an area frame; `.ai` and EPS import: Art brushes in the older form of their objects come into the Brushes panel; EPS import: gradients drawn as abutting strips come in as one gradient, without seams; EPS import: composite fonts draw their Type 42 glyphs; SVG import: filter and mask regions clip, #5876, #5875; SVG import: Gaussian blurs keep `color-interpolation-filters`, #5877; PDF and `.ai` export: objects' URLs as links, #5785; SVG import: `color-interpolation="linearRGB"` gradients, #5878; `.ai` import: paths with a Calligraphic brush open brushed, #372) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-07 · **Change:** minor (EPS import: sampled functions interpolate between their samples; `.ai` and EPS type from the text document keeps its tab stops; it keeps auto leading and hyphenation options, and may show more of the file's last lines in an area frame; `.ai` and EPS import: Art brushes in the older form of their objects come into the Brushes panel; EPS import: gradients drawn as abutting strips come in as one gradient, without seams; EPS import: composite fonts draw their Type 42 glyphs; SVG import: filter and mask regions clip, #5876, #5875; SVG import: Gaussian blurs keep `color-interpolation-filters`, #5877; PDF and `.ai` export: objects' URLs as links, #5785; SVG import: `color-interpolation="linearRGB"` gradients, #5878; `.ai` import: paths with a Calligraphic brush open brushed, #372) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
|
||||
Every format Illustrator opens, places, saves or exports, against VectorCraft's format table (`FORMATS`,
|
||||
`OPEN_EXTS`, `PLACE_EXTS` in `crates/engine/src/cmd/fileio/formats.rs`). Illustrator's list comes from its public
|
||||
@@ -281,7 +281,7 @@ Names in the editing data that aren't UTF-8 (layers, objects, artboards, colors,
|
||||
| Illustrator template `.ait` | open, save | yes | PDF-compatible only, from Save as Template (`.vctemplate` stays its default) | New from Template opens a saved `.ait` untitled with its layers and art (`tests_pdfedit.rs`) |
|
||||
| Illustrator `.ai` | open, save | yes (PDF-compatible + editing-data structure with template layers and ruler guides, File Info) | PDF-compatible, or legacy version 7 (layers and groups) | see above; no real-file corpus |
|
||||
| PDF | open, place, save | yes (layers, masks, editable text, security, overprint, File Info from the XMP packet and the document information dictionary; Crop To › Trim takes the bleed box as the document's bleed) | yes (presets, PDF/X-1a/3/4, layers, marks and bleed, ICC, fast web view, 1.3+; objects' URLs as link annotations, not in PDF/X) | fuzzed import; shared guarded writer and windowed reader ([pdf-backend.md](pdf-backend.md)), metadata, incomplete-content and dependency-source regressions; PDF text opens where the file draws it, glyph by glyph (#722: kerning or none, spacing per character, text rise as baseline shift, vertical writing, justified text boxes); alpha soft masks drawn as images import with the mask (#1551); a clip only counts as covering the page when its shape does (#1542); a PDF another app copied pastes as editable vector art (#1499); letter-spaced type on a path keeps its length and its glyph-by-glyph stroke comes in as one text object (#1586, #1585); a crafted name tree with shared nodes no longer takes exponential time on open (#1781); fonts named with hyphens (`Noto-Sans-Bold`) or with a composite font's CMap (`Name-Identity-H`) find the installed family; invisible text (rendering mode 3: a scanned page's recognized text) opens as type with no fill or stroke and saves back as invisible, searchable text (#2675); PDF/X-1a and X-3 flattening in a CMYK document keeps CMYK inks where it rasterizes (#2160) |
|
||||
| EPS | open, place, save | yes (own PostScript L3 interpreter, Illustrator EPS editing data structure) | yes (L2/L3, TIFF previews) | CID-keyed CJK type not read; #505; images encoded with `CCITTFaxDecode` (ITU-T T.4 Group 3 and T.6 Group 4, with `K`, `Columns`, `Rows`, `EndOfBlock`, `EndOfLine`, `EncodedByteAlign` and `BlackIs1`) draw, and damaged data keeps the rows decoded before its first damaged row; Type 42 (TrueType) fonts set type at its size (#1553); composite (Type 0) fonts with the 8/8 mapping, as PDF-to-PostScript converters write two-byte fonts, draw a Type 42 descendant's glyphs from the TrueType outlines it carries and set other descendants' characters as type at the composite font's size (other mappings and CID-keyed fonts are still set as type in the font of their name); a clip only counts as covering the page when its shape does (#1542); an EPS another app copied pastes as editable vector art (`clipboard.importEps`, #1499); a stitched shading keeps its hard colour changes on import (#1654); a gradient the page paints as a row of abutting one-colour strips (as Level 2 writers draw smooth shades) imports as one linear gradient, without the seams the strips leave when each is drawn alone; CMYK images, CMYK JPEGs included, keep their ink amounts on import, as from PDF, and the pixels a `MaskColor` or `ImageType 3` mask hides become a grayscale opacity mask; ruzstd 0.9.1 for compressed editing data (#1807, RUSTSEC-2026-0344) |
|
||||
| EPS | open, place, save | yes (own PostScript L3 interpreter, Illustrator EPS editing data structure) | yes (L2/L3, TIFF previews) | CID-keyed CJK type not read; #505; images encoded with `CCITTFaxDecode` (ITU-T T.4 Group 3 and T.6 Group 4, with `K`, `Columns`, `Rows`, `EndOfBlock`, `EndOfLine`, `EncodedByteAlign` and `BlackIs1`) draw, and damaged data keeps the rows decoded before its first damaged row; Type 42 (TrueType) fonts set type at its size (#1553); composite (Type 0) fonts with the 8/8 mapping, as PDF-to-PostScript converters write two-byte fonts, draw a Type 42 descendant's glyphs from the TrueType outlines it carries and set other descendants' characters as type at the composite font's size (other mappings and CID-keyed fonts are still set as type in the font of their name); a clip only counts as covering the page when its shape does (#1542); an EPS another app copied pastes as editable vector art (`clipboard.importEps`, #1499); a stitched shading keeps its hard colour changes on import (#1654); sampled functions (type 0) interpolate between their samples, multilinearly or by cubics with `Order 3`, and a sampled axial or radial gradient has a stop at each sample; a gradient the page paints as a row of abutting one-colour strips (as Level 2 writers draw smooth shades) imports as one linear gradient, without the seams the strips leave when each is drawn alone; CMYK images, CMYK JPEGs included, keep their ink amounts on import, as from PDF, and the pixels a `MaskColor` or `ImageType 3` mask hides become a grayscale opacity mask; ruzstd 0.9.1 for compressed editing data (#1807, RUSTSEC-2026-0344) |
|
||||
| PostScript `.ps` | open, place | yes (the EPS reader reads its first page; `.epsf` files open and place as EPS) | via Print to PostScript | `tests_epsimport.rs` |
|
||||
| SVG / SVGZ | open, place, save | yes | yes (SVG Options, Preserve Editing, symbols, filters, rich text) | strongest format; tests in `crates/svg/tests`; text on a path follows basic shapes as their SVG 2 equivalent paths (#1527); CSS transforms and the `font` shorthand (#1565), percentage lengths in symbols (#1596), Inkscape flowed text as area type (#1568) and Inkscape pages as artboards (#1601) import; an unsupported filter is named in its warning (#1598); an artboard exported on its own (Export As with Use Artboards, Export for Screens, Asset Export) has the artboard's name as `<title>`, Save the document's (#3544); attributes in other applications' XML namespaces (`plotter:pen="2"`) kept on their objects from import to export, as `data-*` attributes are (`crates/svg/tests/it/foreign_attributes.rs`); simple shapes export as `<rect>`, `<circle>`, `<ellipse>`, `<line>`, `<polygon>` and `<polyline>` where they draw the same as the path (#4652, `crates/svg/tests/it/basic_shapes.rs`) |
|
||||
| DXF | open, export | yes | yes (R12–2018) | R13–2018 closed SPLINE flags and control-point closure on import verified (#1299); circular cubic paths export as native CIRCLE/ARC where stroke semantics permit; R12 weighted strokes remain POLYLINE while non-circular curves keep the existing fallback (`crates/cad/src/tests.rs`); [Autodesk group 70](https://help.autodesk.com/cloudhelp/2024/ENU/AutoCAD-DXF/files/GUID-E1F884F8-AA90-4864-A215-3182D47A9C74.htm). DXF 2000–2018 LAYOUT objects write the paper image origin as 148/149 (was a stray 158), the shade plot codes (2004+) and the model type flag, and the plot style placeholder carries no subclass marker (#3085; structure test `r2000_and_later_files_have_the_structure_cad_apps_require`, checked with ezdxf's audit; not yet confirmed in Autodesk Fusion). CNC preset and VCarve QA remain open |
|
||||
@@ -320,6 +320,7 @@ Names in the editing data that aren't UTF-8 (layers, objects, artboards, colors,
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-07 | minor | EPS import: a sampled function (type 0) of two or more inputs took the nearest sample, so a function-based shading (type 1) over one came in as flat blocks; it is now interpolated between its samples (multilinearly), and with `Order 3` by cubics through them (Catmull-Rom, kept in the function's range), as the PostScript Language Reference (3rd ed., §3.10.1, type 0 functions) defines. An axial or radial shading whose function is a linearly interpolated sampled function of one input has a stop at each sample, not 33 evenly spaced ones. Tests `sampled_functions_interpolate_linearly_or_by_cubics`, `mesh_and_function_shadings_become_gradient_meshes`; a 2 × 2 function-based shading is 1.2% from Ghostscript's render (it was 45.9%); scores unchanged |
|
||||
| 2026-11-07 | minor | EPS and `.ai` type from the text document keeps its paragraphs' tab stops (positions and alignments; leaders aren't read), at most 1,024 a paragraph and 2^20 a story, past which the default stops apply with a warning. A user's `.ai` (used locally, not committed) whose price lists came in on the default stops now lays them out as its page has them (its type 0.8% from its page, from 1.5%; what differs is hyphenation in Hungarian and type in a font that isn't installed). Scores and estimates unchanged. |
|
||||
| 2026-11-07 | minor | EPS and `.ai` type from the text document: auto leading stays auto at the paragraph's share (it was the first two lines' distance for every line, so 10 pt lines under a 15 pt heading came in 18 pt apart), the paragraph's hyphenation options are read, and area type of one frame whose last lines the file sets in too short a frame for them here may get a frame taller by its lines' deepest descent plus 0.05 pt, when that shows more of them. A user's `.ai` (used locally, not committed) whose type differed from its page by 6.6%, so that it opened from its PDF part, now opens from its layers (4.9%; type with tab stops in right-aligned paragraphs still differs, a separate issue) (#6137). Scores and estimates unchanged. |
|
||||
| 2026-11-06 | minor | EPS and `.ai` editing data: a path or compound path whose art style stacks several fills and strokes (solid colours and gradients, each with its own visibility, opacity and blend mode, under effects any object takes) comes in as itself with that appearance instead of its drawn look. Each gradient must be its own swatch, placed as the drawn look places it; a stack that repeats a swatch, or whose look doesn't paint its gradients, keeps the drawn look, as do fills and strokes with effects of their own. |
|
||||
|
||||
Reference in New Issue
Block a user