use super::color::{HIDDEN_LAYERS, PLATE, color_of, overprints, plate_pixmap, screen_color, set_fill_paint}; use super::pdf_raster::{is_pdf, shown_box}; use super::types::{DisplayQuality, Frame}; use super::{ Affine, BezPath, BlendMode, Compose, Item, Mix, Rect, RenderContext, Renderer, Shape, StrokeAlign, StrokeType, colorize, images, kurbo, pdf_layers, peniko, vello_cpu, }; impl Renderer { pub(crate) 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 (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, designcraft_doc::Cap::Round => kurbo::Cap::Round, designcraft_doc::Cap::Projecting => kurbo::Cap::Square, }) .with_join(match st.join { designcraft_doc::Join::Miter => kurbo::Join::Miter, designcraft_doc::Join::Round => kurbo::Join::Round, designcraft_doc::Join::Bevel => kurbo::Join::Bevel, }) .with_miter_limit(st.miter_limit); let kind = f.doc.stroke_kind(&st.kind); let length = designcraft_doc::path_length_bound(bp); match &kind { StrokeType::Dashed { pattern } if StrokeType::expandable_dashes(pattern, length) => { stroke = stroke.with_dashes(0.0, pattern.iter().copied()) } StrokeType::Dotted if StrokeType::expandable_dashes(&[0.0, st.weight * 2.0], length) => { stroke = stroke.with_dashes(0.0, [0.0, st.weight * 2.0]).with_caps(kurbo::Cap::Round) } _ => {} } ctx.set_transform(f.view * xf); 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); let bp = arrows.as_ref().map_or(bp, |a| &a.0); // Gap colour under dashes and dots: the whole path, undashed. let gap = if matches!(kind, StrokeType::Solid) { None } else { f.doc.resolve_color(&st.gap_swatch, st.gap_tint) }; if let Some(g) = gap && !matches!(st.align, StrokeAlign::Inside | StrokeAlign::Outside if closed) { let op = overprints(f, &st.gap_swatch, st.gap_tint, st.gap_overprint); if op { ctx.set_transform(Affine::IDENTITY); ctx.push_layer(None, Some(BlendMode::new(Mix::Multiply, Compose::SrcOver)), None, None, None); ctx.set_transform(f.view * xf); } ctx.set_paint(color_of(&g, 1.0)); ctx.set_stroke(kurbo::Stroke { dash_pattern: Default::default(), start_cap: kurbo::Cap::Butt, end_cap: kurbo::Cap::Butt, ..stroke.clone() }); ctx.stroke_path(bp); if op { ctx.pop_layer(); } 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) { ctx.fill_path(&o); for h in arrows.iter().flat_map(|a| &a.1) { match h.outline { Some(w) => { ctx.set_stroke(kurbo::Stroke::new(w).with_join(kurbo::Join::Miter)); ctx.stroke_path(&h.path); } None => ctx.fill_path(&h.path), } } return; } match st.align { StrokeAlign::Inside if closed => { // Clip a double-width stroke to the path. ctx.push_clip_layer(bp); ctx.set_stroke(kurbo::Stroke { width: st.weight * 2.0, ..stroke }); ctx.stroke_path(bp); ctx.pop_layer(); } StrokeAlign::Outside if closed => { // Subtract the filled path from a double-width stroke. Combining a stroked // outline (already hollow) with the path using even-odd would paint its centre. // Isolate the stroke so the subtraction leaves the item's fill/content and // backdrop intact; the same nonzero fill rule also preserves compound holes. ctx.push_layer(None, None, None, None, None); ctx.set_stroke(kurbo::Stroke { width: st.weight * 2.0, ..stroke }); ctx.stroke_path(bp); ctx.push_layer(None, Some(BlendMode::new(Mix::Normal, Compose::DestOut)), None, None, None); ctx.fill_path(bp); ctx.pop_layer(); ctx.pop_layer(); } _ => { ctx.set_stroke(stroke); ctx.stroke_path(bp); } } for h in arrows.iter().flat_map(|a| &a.1) { match h.outline { Some(w) => { ctx.set_stroke(kurbo::Stroke::new(w).with_join(kurbo::Join::Miter)); ctx.stroke_path(&h.path); } None => ctx.fill_path(&h.path), } } } /// Draw a placed graphic, inside its clipping path when it has one. pub(crate) fn draw_graphic(&mut self, ctx: &mut RenderContext, f: &Frame, g: &designcraft_doc::Graphic, xf: Affine) { let Some((region, even_odd)) = g.clip_region() else { self.draw_graphic_unclipped(ctx, f, g, xf); return; }; ctx.set_transform(f.view * xf); if even_odd { ctx.set_fill_rule(peniko::Fill::EvenOdd); } ctx.push_clip_layer(®ion); ctx.set_fill_rule(peniko::Fill::NonZero); self.draw_graphic_unclipped(ctx, f, g, xf); ctx.pop_layer(); } fn draw_graphic_unclipped(&mut self, ctx: &mut RenderContext, f: &Frame, g: &designcraft_doc::Graphic, xf: Affine) { let Some(asset) = f.doc.assets.get(&g.asset) else { return }; if designcraft_doc::media_kind(&asset.mime).is_some() { // Video / sound: a dark frame with a play mark (the poster, when set, is drawn by the item). ctx.set_transform(f.view * xf * g.xf); ctx.set_paint(screen_color([48, 48, 52], 255)); ctx.fill_rect(&Rect::new(0.0, 0.0, g.size.0, g.size.1)); let (cx, cy, r) = (g.size.0 / 2.0, g.size.1 / 2.0, g.size.0.min(g.size.1) * 0.18); let mut tri = BezPath::new(); tri.move_to((cx - r * 0.6, cy - r)); tri.line_to((cx + r, cy)); tri.line_to((cx - r * 0.6, cy + r)); tri.close_path(); ctx.set_paint(screen_color([235, 235, 235], 255)); ctx.fill_path(&tri); return; } if f.opts.quality == DisplayQuality::Fast { ctx.set_transform(f.view * xf * g.xf); ctx.set_paint(screen_color([178, 178, 178], 255)); ctx.fill_rect(&Rect::new(0.0, 0.0, g.size.0, g.size.1)); return; } // Pick a mip level close to the on-screen size (Typical: a 72 ppi proxy). let on_screen = (f.view * xf * g.xf).determinant().abs().sqrt(); let on_screen = if f.opts.quality == DisplayQuality::Typical { on_screen.min(1.0) } else { on_screen }; let hidden = HIDDEN_LAYERS.with(|h| h.borrow().clone()); let data = match hidden.as_deref().filter(|_| is_pdf(&asset.data)) { // An explicit choice (including all-on) that cannot be rewritten fails closed: // stale persisted/relinked choices must not reveal hidden source content. Some(hidden) => { let Some(variant) = pdf_layers::layered(&asset.data, hidden) else { return }; variant } None => asset.data.clone(), }; // A placed PDF cropped to a box: the whole page, placed so that the box spans graphic space. let [l, t, r, b] = shown_box(asset).unwrap_or([0.0, 0.0, 1.0, 1.0]); let (page_w, page_h) = (g.size.0 / (r - l), g.size.1 / (b - t)); let page = Rect::new(-l * page_w, -t * page_h, (1.0 - l) * page_w, (1.0 - t) * page_h); let Some(pm) = images::mip(&data, asset.page, page_w, on_screen) else { return }; // A coloured image is already in the plate's inks (its colours come from `color_of`). let pm = match (colorize::colorized(f.doc, &g.fill, &asset.data, &pm), PLATE.with(std::cell::Cell::get)) { (Some(coloured), _) => coloured, (None, Some(plate)) => plate_pixmap(&pm, plate), (None, None) => pm, }; // Only where the page is: a box reaching past it (a media box larger than the crop box) // shows nothing there. let rect = Rect::new(0.0, 0.0, g.size.0, g.size.1).intersect(page); ctx.set_transform(f.view * xf * g.xf); let sx = page_w / pm.width().max(1) as f64; let sy = page_h / pm.height().max(1) as f64; ctx.set_paint(vello_cpu::Image { image: vello_cpu::ImageSource::Pixmap(pm), sampler: peniko::ImageSampler::default().with_quality(peniko::ImageQuality::Medium), }); ctx.set_paint_transform(Affine::translate((page.x0, page.y0)) * Affine::scale_non_uniform(sx, sy)); ctx.fill_rect(&rect); ctx.reset_paint_transform(); } }