Implement interior miter line joins (not yet endpoints)

This commit is contained in:
Patrick Walton 2019-05-16 11:10:15 -07:00
parent 272b63a017
commit e282eb57d5
3 changed files with 91 additions and 23 deletions

View File

@ -19,7 +19,7 @@ use pathfinder_geometry::basic::rect::RectF32;
use pathfinder_geometry::basic::transform2d::Transform2DF32; use pathfinder_geometry::basic::transform2d::Transform2DF32;
use pathfinder_geometry::color::ColorU; use pathfinder_geometry::color::ColorU;
use pathfinder_geometry::outline::{Contour, Outline}; use pathfinder_geometry::outline::{Contour, Outline};
use pathfinder_geometry::stroke::{LineCap, OutlineStrokeToFill, StrokeStyle}; use pathfinder_geometry::stroke::{LineCap, LineJoin, OutlineStrokeToFill, StrokeStyle};
use pathfinder_renderer::paint::Paint; use pathfinder_renderer::paint::Paint;
use pathfinder_renderer::scene::{PathObject, Scene}; use pathfinder_renderer::scene::{PathObject, Scene};
use pathfinder_text::{SceneExt, TextRenderMode}; use pathfinder_text::{SceneExt, TextRenderMode};
@ -132,6 +132,11 @@ impl CanvasRenderingContext2D {
self.current_state.stroke_style.line_cap = new_line_cap self.current_state.stroke_style.line_cap = new_line_cap
} }
#[inline]
pub fn set_line_join(&mut self, new_line_join: LineJoin) {
self.current_state.stroke_style.line_join = new_line_join
}
#[inline] #[inline]
pub fn set_fill_style(&mut self, new_fill_style: FillStyle) { pub fn set_fill_style(&mut self, new_fill_style: FillStyle) {
self.current_state.fill_paint = new_fill_style.to_paint(); self.current_state.fill_paint = new_fill_style.to_paint();

View File

@ -28,6 +28,7 @@ pub struct OutlineStrokeToFill {
pub struct StrokeStyle { pub struct StrokeStyle {
pub line_width: f32, pub line_width: f32,
pub line_cap: LineCap, pub line_cap: LineCap,
pub line_join: LineJoin,
} }
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
@ -36,6 +37,12 @@ pub enum LineCap {
Square, Square,
} }
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum LineJoin {
Miter,
Bevel,
}
impl OutlineStrokeToFill { impl OutlineStrokeToFill {
#[inline] #[inline]
pub fn new(outline: Outline, style: StrokeStyle) -> OutlineStrokeToFill { pub fn new(outline: Outline, style: StrokeStyle) -> OutlineStrokeToFill {
@ -48,21 +55,26 @@ impl OutlineStrokeToFill {
let closed = input.closed; let closed = input.closed;
let mut stroker = ContourStrokeToFill::new(input, let mut stroker = ContourStrokeToFill::new(input,
Contour::new(), Contour::new(),
self.style.line_width * 0.5); self.style.line_width * 0.5,
self.style.line_join);
stroker.offset_forward(); stroker.offset_forward();
if closed { if closed {
// TODO(pcwalton): Line join.
stroker.output.closed = true; stroker.output.closed = true;
new_contours.push(stroker.output); new_contours.push(stroker.output);
stroker = ContourStrokeToFill::new(stroker.input, stroker = ContourStrokeToFill::new(stroker.input,
Contour::new(), Contour::new(),
self.style.line_width * 0.5); self.style.line_width * 0.5,
self.style.line_join);
} else { } else {
self.add_cap(&mut stroker.output); self.add_cap(&mut stroker.output);
} }
stroker.offset_backward(); stroker.offset_backward();
if !closed { if closed {
// TODO(pcwalton): Line join.
} else {
self.add_cap(&mut stroker.output); self.add_cap(&mut stroker.output);
} }
@ -101,21 +113,18 @@ struct ContourStrokeToFill {
input: Contour, input: Contour,
output: Contour, output: Contour,
radius: f32, radius: f32,
join: LineJoin,
} }
impl ContourStrokeToFill { impl ContourStrokeToFill {
#[inline] #[inline]
fn new(input: Contour, output: Contour, radius: f32) -> ContourStrokeToFill { fn new(input: Contour, output: Contour, radius: f32, join: LineJoin) -> ContourStrokeToFill {
ContourStrokeToFill { ContourStrokeToFill { input, output, radius, join }
input,
output,
radius,
}
} }
fn offset_forward(&mut self) { fn offset_forward(&mut self) {
for segment in self.input.iter() { for segment in self.input.iter() {
segment.offset(self.radius, &mut self.output); segment.offset(self.radius, self.join, &mut self.output);
} }
} }
@ -127,27 +136,28 @@ impl ContourStrokeToFill {
.collect(); .collect();
segments.reverse(); segments.reverse();
for segment in &segments { for segment in &segments {
segment.offset(self.radius, &mut self.output); segment.offset(self.radius, self.join, &mut self.output);
} }
} }
} }
trait Offset { trait Offset {
fn offset(&self, distance: f32, contour: &mut Contour); fn offset(&self, distance: f32, join: LineJoin, contour: &mut Contour);
fn add_to_contour(&self, join: LineJoin, contour: &mut Contour);
fn offset_once(&self, distance: f32) -> Self; fn offset_once(&self, distance: f32) -> Self;
fn error_is_within_tolerance(&self, other: &Segment, distance: f32) -> bool; fn error_is_within_tolerance(&self, other: &Segment, distance: f32) -> bool;
} }
impl Offset for Segment { impl Offset for Segment {
fn offset(&self, distance: f32, contour: &mut Contour) { fn offset(&self, distance: f32, join: LineJoin, contour: &mut Contour) {
if self.baseline.square_length() < TOLERANCE * TOLERANCE { if self.baseline.square_length() < TOLERANCE * TOLERANCE {
contour.push_full_segment(self, true); self.add_to_contour(join, contour);
return; return;
} }
let candidate = self.offset_once(distance); let candidate = self.offset_once(distance);
if self.error_is_within_tolerance(&candidate, distance) { if self.error_is_within_tolerance(&candidate, distance) {
contour.push_full_segment(&candidate, true); candidate.add_to_contour(join, contour);
return; return;
} }
@ -155,8 +165,33 @@ impl Offset for Segment {
debug!("... PRE-SPLIT: {:?}", self); debug!("... PRE-SPLIT: {:?}", self);
let (before, after) = self.split(0.5); let (before, after) = self.split(0.5);
debug!("... AFTER-SPLIT: {:?} {:?}", before, after); debug!("... AFTER-SPLIT: {:?} {:?}", before, after);
before.offset(distance, contour); before.offset(distance, join, contour);
after.offset(distance, contour); after.offset(distance, join, contour);
}
fn add_to_contour(&self, join: LineJoin, contour: &mut Contour) {
// Add join.
// TODO(pcwalton): Miter limit.
if join == LineJoin::Miter && contour.len() >= 2 {
let (p0, p1) = (contour.position_of_last(2), contour.position_of_last(1));
let p3 = self.baseline.from();
let p4 = if self.is_line() {
self.baseline.to()
} else {
// NB: If you change the representation of quadratic curves, you will need to
// change this.
self.ctrl.from()
};
let prev_tangent = LineSegmentF32::new(p0, p1);
let next_tangent = LineSegmentF32::new(p4, p3);
if let Some(prev_tangent_t) = prev_tangent.intersection_t(&next_tangent) {
contour.push_endpoint(prev_tangent.sample(prev_tangent_t));
}
}
// Push segment.
contour.push_full_segment(self, true);
} }
fn offset_once(&self, distance: f32) -> Segment { fn offset_once(&self, distance: f32) -> Segment {
@ -260,7 +295,11 @@ impl Offset for Segment {
impl Default for StrokeStyle { impl Default for StrokeStyle {
#[inline] #[inline]
fn default() -> StrokeStyle { fn default() -> StrokeStyle {
StrokeStyle { line_width: 1.0, line_cap: LineCap::default() } StrokeStyle {
line_width: 1.0,
line_cap: LineCap::default(),
line_join: LineJoin::default(),
}
} }
} }
@ -268,3 +307,8 @@ impl Default for LineCap {
#[inline] #[inline]
fn default() -> LineCap { LineCap::Butt } fn default() -> LineCap { LineCap::Butt }
} }
impl Default for LineJoin {
#[inline]
fn default() -> LineJoin { LineJoin::Miter }
}

View File

@ -20,14 +20,14 @@ use pathfinder_geometry::basic::transform2d::{Transform2DF32, Transform2DF32Path
use pathfinder_geometry::color::ColorU; use pathfinder_geometry::color::ColorU;
use pathfinder_geometry::outline::Outline; use pathfinder_geometry::outline::Outline;
use pathfinder_geometry::segment::{Segment, SegmentFlags}; use pathfinder_geometry::segment::{Segment, SegmentFlags};
use pathfinder_geometry::stroke::{LineCap, OutlineStrokeToFill, StrokeStyle}; use pathfinder_geometry::stroke::{LineCap, LineJoin, OutlineStrokeToFill, StrokeStyle};
use pathfinder_renderer::paint::Paint; use pathfinder_renderer::paint::Paint;
use pathfinder_renderer::scene::{PathObject, Scene}; use pathfinder_renderer::scene::{PathObject, Scene};
use std::fmt::{Display, Formatter, Result as FormatResult}; use std::fmt::{Display, Formatter, Result as FormatResult};
use std::mem; use std::mem;
use usvg::{Color as SvgColor, LineCap as UsvgLineCap, Node, NodeExt, NodeKind, Opacity}; use usvg::{Color as SvgColor, LineCap as UsvgLineCap, LineJoin as UsvgLineJoin, Node, NodeExt};
use usvg::{Paint as UsvgPaint, PathSegment as UsvgPathSegment, Rect as UsvgRect}; use usvg::{NodeKind, Opacity, Paint as UsvgPaint, PathSegment as UsvgPathSegment};
use usvg::{Transform as UsvgTransform, Tree, Visibility}; use usvg::{Rect as UsvgRect, Transform as UsvgTransform, Tree, Visibility};
const HAIRLINE_STROKE_WIDTH: f32 = 0.0333; const HAIRLINE_STROKE_WIDTH: f32 = 0.0333;
@ -139,6 +139,7 @@ impl BuiltSVG {
let stroke_style = StrokeStyle { let stroke_style = StrokeStyle {
line_width: f32::max(stroke.width.value() as f32, HAIRLINE_STROKE_WIDTH), line_width: f32::max(stroke.width.value() as f32, HAIRLINE_STROKE_WIDTH),
line_cap: LineCap::from_usvg_line_cap(stroke.linecap), line_cap: LineCap::from_usvg_line_cap(stroke.linecap),
line_join: LineJoin::from_usvg_line_join(stroke.linejoin),
}; };
let path = UsvgPathToSegments::new(path.segments.iter().cloned()); let path = UsvgPathToSegments::new(path.segments.iter().cloned());
@ -400,3 +401,21 @@ impl LineCapExt for LineCap {
} }
} }
} }
trait LineJoinExt {
fn from_usvg_line_join(usvg_line_join: UsvgLineJoin) -> Self;
}
impl LineJoinExt for LineJoin {
#[inline]
fn from_usvg_line_join(usvg_line_join: UsvgLineJoin) -> LineJoin {
match usvg_line_join {
UsvgLineJoin::Miter => LineJoin::Miter,
UsvgLineJoin::Round => {
// TODO(pcwalton)
LineJoin::Miter
}
UsvgLineJoin::Bevel => LineJoin::Bevel,
}
}
}