mirror of
https://github.com/typst/typst
synced 2025-05-14 17:15:28 +08:00
1326 lines
40 KiB
Rust
1326 lines
40 KiB
Rust
use std::f64::consts::SQRT_2;
|
|
|
|
use kurbo::{CubicBez, ParamCurveExtrema};
|
|
use typst_library::diag::{bail, SourceResult};
|
|
use typst_library::engine::Engine;
|
|
use typst_library::foundations::{Content, Packed, Resolve, Smart, StyleChain};
|
|
use typst_library::introspection::Locator;
|
|
use typst_library::layout::{
|
|
Abs, Axes, Corner, Corners, Frame, FrameItem, Length, Point, Ratio, Region, Rel,
|
|
Sides, Size,
|
|
};
|
|
use typst_library::visualize::{
|
|
CircleElem, CloseMode, Curve, CurveComponent, CurveElem, EllipseElem, FillRule,
|
|
FixedStroke, Geometry, LineElem, Paint, PathElem, PathVertex, PolygonElem, RectElem,
|
|
Shape, SquareElem, Stroke,
|
|
};
|
|
use typst_syntax::Span;
|
|
use typst_utils::{Get, Numeric};
|
|
|
|
/// Layout the line.
|
|
#[typst_macros::time(span = elem.span())]
|
|
pub fn layout_line(
|
|
elem: &Packed<LineElem>,
|
|
_: &mut Engine,
|
|
_: Locator,
|
|
styles: StyleChain,
|
|
region: Region,
|
|
) -> SourceResult<Frame> {
|
|
let resolve = |axes: Axes<Rel<Abs>>| axes.zip_map(region.size, Rel::relative_to);
|
|
let start = resolve(elem.start(styles));
|
|
let delta = elem.end(styles).map(|end| resolve(end) - start).unwrap_or_else(|| {
|
|
let length = elem.length(styles);
|
|
let angle = elem.angle(styles);
|
|
let x = angle.cos() * length;
|
|
let y = angle.sin() * length;
|
|
resolve(Axes::new(x, y))
|
|
});
|
|
|
|
let stroke = elem.stroke(styles).unwrap_or_default();
|
|
let size = start.max(start + delta).max(Size::zero());
|
|
|
|
if !size.is_finite() {
|
|
bail!(elem.span(), "cannot create line with infinite length");
|
|
}
|
|
|
|
let mut frame = Frame::soft(size);
|
|
let shape = Geometry::Line(delta.to_point()).stroked(stroke);
|
|
frame.push(start.to_point(), FrameItem::Shape(shape, elem.span()));
|
|
Ok(frame)
|
|
}
|
|
|
|
/// Layout the path.
|
|
#[typst_macros::time(span = elem.span())]
|
|
pub fn layout_path(
|
|
elem: &Packed<PathElem>,
|
|
_: &mut Engine,
|
|
_: Locator,
|
|
styles: StyleChain,
|
|
region: Region,
|
|
) -> SourceResult<Frame> {
|
|
let resolve = |axes: Axes<Rel<Length>>| {
|
|
axes.resolve(styles).zip_map(region.size, Rel::relative_to).to_point()
|
|
};
|
|
|
|
let vertices = &elem.vertices;
|
|
let points: Vec<Point> = vertices.iter().map(|c| resolve(c.vertex())).collect();
|
|
|
|
let mut size = Size::zero();
|
|
if points.is_empty() {
|
|
return Ok(Frame::soft(size));
|
|
}
|
|
|
|
// Only create a path if there are more than zero points.
|
|
// Construct a closed path given all points.
|
|
let mut curve = Curve::new();
|
|
curve.move_(points[0]);
|
|
|
|
let mut add_cubic = |from_point: Point,
|
|
to_point: Point,
|
|
from: PathVertex,
|
|
to: PathVertex| {
|
|
let from_control_point = resolve(from.control_point_from()) + from_point;
|
|
let to_control_point = resolve(to.control_point_to()) + to_point;
|
|
curve.cubic(from_control_point, to_control_point, to_point);
|
|
|
|
let p0 = kurbo::Point::new(from_point.x.to_raw(), from_point.y.to_raw());
|
|
let p1 = kurbo::Point::new(
|
|
from_control_point.x.to_raw(),
|
|
from_control_point.y.to_raw(),
|
|
);
|
|
let p2 =
|
|
kurbo::Point::new(to_control_point.x.to_raw(), to_control_point.y.to_raw());
|
|
let p3 = kurbo::Point::new(to_point.x.to_raw(), to_point.y.to_raw());
|
|
let extrema = kurbo::CubicBez::new(p0, p1, p2, p3).bounding_box();
|
|
size.x.set_max(Abs::raw(extrema.x1));
|
|
size.y.set_max(Abs::raw(extrema.y1));
|
|
};
|
|
|
|
for (vertex_window, point_window) in vertices.windows(2).zip(points.windows(2)) {
|
|
let from = vertex_window[0];
|
|
let to = vertex_window[1];
|
|
let from_point = point_window[0];
|
|
let to_point = point_window[1];
|
|
|
|
add_cubic(from_point, to_point, from, to);
|
|
}
|
|
|
|
if elem.closed(styles) {
|
|
let from = *vertices.last().unwrap(); // We checked that we have at least one element.
|
|
let to = vertices[0];
|
|
let from_point = *points.last().unwrap();
|
|
let to_point = points[0];
|
|
|
|
add_cubic(from_point, to_point, from, to);
|
|
curve.close();
|
|
}
|
|
|
|
if !size.is_finite() {
|
|
bail!(elem.span(), "cannot create path with infinite length");
|
|
}
|
|
|
|
// Prepare fill and stroke.
|
|
let fill = elem.fill(styles);
|
|
let fill_rule = elem.fill_rule(styles);
|
|
let stroke = match elem.stroke(styles) {
|
|
Smart::Auto if fill.is_none() => Some(FixedStroke::default()),
|
|
Smart::Auto => None,
|
|
Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default),
|
|
};
|
|
|
|
let mut frame = Frame::soft(size);
|
|
let shape = Shape {
|
|
geometry: Geometry::Curve(curve),
|
|
stroke,
|
|
fill,
|
|
fill_rule,
|
|
};
|
|
frame.push(Point::zero(), FrameItem::Shape(shape, elem.span()));
|
|
Ok(frame)
|
|
}
|
|
|
|
/// Layout the curve.
|
|
#[typst_macros::time(span = elem.span())]
|
|
pub fn layout_curve(
|
|
elem: &Packed<CurveElem>,
|
|
_: &mut Engine,
|
|
_: Locator,
|
|
styles: StyleChain,
|
|
region: Region,
|
|
) -> SourceResult<Frame> {
|
|
let mut builder = CurveBuilder::new(region, styles);
|
|
|
|
for item in &elem.components {
|
|
match item {
|
|
CurveComponent::Move(element) => {
|
|
let relative = element.relative(styles);
|
|
let point = builder.resolve_point(element.start, relative);
|
|
builder.move_(point);
|
|
}
|
|
|
|
CurveComponent::Line(element) => {
|
|
let relative = element.relative(styles);
|
|
let point = builder.resolve_point(element.end, relative);
|
|
builder.line(point);
|
|
}
|
|
|
|
CurveComponent::Quad(element) => {
|
|
let relative = element.relative(styles);
|
|
let end = builder.resolve_point(element.end, relative);
|
|
let control = match element.control {
|
|
Smart::Auto => {
|
|
control_c2q(builder.last_point, builder.last_control_from)
|
|
}
|
|
Smart::Custom(Some(p)) => builder.resolve_point(p, relative),
|
|
Smart::Custom(None) => end,
|
|
};
|
|
builder.quad(control, end);
|
|
}
|
|
|
|
CurveComponent::Cubic(element) => {
|
|
let relative = element.relative(styles);
|
|
let end = builder.resolve_point(element.end, relative);
|
|
let c1 = match element.control_start {
|
|
Some(Smart::Custom(p)) => builder.resolve_point(p, relative),
|
|
Some(Smart::Auto) => builder.last_control_from,
|
|
None => builder.last_point,
|
|
};
|
|
let c2 = match element.control_end {
|
|
Some(p) => builder.resolve_point(p, relative),
|
|
None => end,
|
|
};
|
|
builder.cubic(c1, c2, end);
|
|
}
|
|
|
|
CurveComponent::Close(element) => {
|
|
builder.close(element.mode(styles));
|
|
}
|
|
}
|
|
}
|
|
|
|
let (curve, size) = builder.finish();
|
|
if curve.is_empty() {
|
|
return Ok(Frame::soft(size));
|
|
}
|
|
|
|
if !size.is_finite() {
|
|
bail!(elem.span(), "cannot create curve with infinite size");
|
|
}
|
|
|
|
// Prepare fill and stroke.
|
|
let fill = elem.fill(styles);
|
|
let fill_rule = elem.fill_rule(styles);
|
|
let stroke = match elem.stroke(styles) {
|
|
Smart::Auto if fill.is_none() => Some(FixedStroke::default()),
|
|
Smart::Auto => None,
|
|
Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default),
|
|
};
|
|
|
|
let mut frame = Frame::soft(size);
|
|
let shape = Shape {
|
|
geometry: Geometry::Curve(curve),
|
|
stroke,
|
|
fill,
|
|
fill_rule,
|
|
};
|
|
frame.push(Point::zero(), FrameItem::Shape(shape, elem.span()));
|
|
Ok(frame)
|
|
}
|
|
|
|
/// Builds a `Curve` from a [`CurveElem`]'s parts.
|
|
struct CurveBuilder<'a> {
|
|
/// The output curve.
|
|
curve: Curve,
|
|
/// The curve's bounds.
|
|
size: Size,
|
|
/// The region relative to which points are resolved.
|
|
region: Region,
|
|
/// The styles for the curve.
|
|
styles: StyleChain<'a>,
|
|
/// The next start point.
|
|
start_point: Point,
|
|
/// Mirror of the first cubic start control point (for closing).
|
|
start_control_into: Point,
|
|
/// The point we previously ended on.
|
|
last_point: Point,
|
|
/// Mirror of the last cubic control point (for auto control points).
|
|
last_control_from: Point,
|
|
/// Whether a component has been start. This does not mean that something
|
|
/// has been added to `self.curve` yet.
|
|
is_started: bool,
|
|
/// Whether anything was added to `self.curve` for the current component.
|
|
is_empty: bool,
|
|
}
|
|
|
|
impl<'a> CurveBuilder<'a> {
|
|
/// Create a new curve builder.
|
|
fn new(region: Region, styles: StyleChain<'a>) -> Self {
|
|
Self {
|
|
curve: Curve::new(),
|
|
size: Size::zero(),
|
|
region,
|
|
styles,
|
|
start_point: Point::zero(),
|
|
start_control_into: Point::zero(),
|
|
last_point: Point::zero(),
|
|
last_control_from: Point::zero(),
|
|
is_started: false,
|
|
is_empty: true,
|
|
}
|
|
}
|
|
|
|
/// Finish building, returning the curve and its bounding size.
|
|
fn finish(self) -> (Curve, Size) {
|
|
(self.curve, self.size)
|
|
}
|
|
|
|
/// Move to a point, starting a new segment.
|
|
fn move_(&mut self, point: Point) {
|
|
// Delay calling `curve.move` in case there is another move element
|
|
// before any actual drawing.
|
|
self.expand_bounds(point);
|
|
self.start_point = point;
|
|
self.start_control_into = point;
|
|
self.last_point = point;
|
|
self.last_control_from = point;
|
|
self.is_started = true;
|
|
}
|
|
|
|
/// Add a line segment.
|
|
fn line(&mut self, point: Point) {
|
|
if self.is_empty {
|
|
self.start_component();
|
|
self.start_control_into = self.start_point;
|
|
}
|
|
self.curve.line(point);
|
|
self.expand_bounds(point);
|
|
self.last_point = point;
|
|
self.last_control_from = point;
|
|
}
|
|
|
|
/// Add a quadratic curve segment.
|
|
fn quad(&mut self, control: Point, end: Point) {
|
|
let c1 = control_q2c(self.last_point, control);
|
|
let c2 = control_q2c(end, control);
|
|
self.cubic(c1, c2, end);
|
|
}
|
|
|
|
/// Add a cubic curve segment.
|
|
fn cubic(&mut self, c1: Point, c2: Point, end: Point) {
|
|
if self.is_empty {
|
|
self.start_component();
|
|
self.start_control_into = mirror_c(self.start_point, c1);
|
|
}
|
|
self.curve.cubic(c1, c2, end);
|
|
|
|
let p0 = point_to_kurbo(self.last_point);
|
|
let p1 = point_to_kurbo(c1);
|
|
let p2 = point_to_kurbo(c2);
|
|
let p3 = point_to_kurbo(end);
|
|
let extrema = CubicBez::new(p0, p1, p2, p3).bounding_box();
|
|
self.size.x.set_max(Abs::raw(extrema.x1));
|
|
self.size.y.set_max(Abs::raw(extrema.y1));
|
|
|
|
self.last_point = end;
|
|
self.last_control_from = mirror_c(end, c2);
|
|
}
|
|
|
|
/// Close the curve if it was opened.
|
|
fn close(&mut self, mode: CloseMode) {
|
|
if self.is_started && !self.is_empty {
|
|
if mode == CloseMode::Smooth {
|
|
self.cubic(
|
|
self.last_control_from,
|
|
self.start_control_into,
|
|
self.start_point,
|
|
);
|
|
}
|
|
self.curve.close();
|
|
self.last_point = self.start_point;
|
|
self.last_control_from = self.start_point;
|
|
}
|
|
self.is_started = false;
|
|
self.is_empty = true;
|
|
}
|
|
|
|
/// Push the initial move component.
|
|
fn start_component(&mut self) {
|
|
self.curve.move_(self.start_point);
|
|
self.is_empty = false;
|
|
self.is_started = true;
|
|
}
|
|
|
|
/// Expand the curve's bounding box.
|
|
fn expand_bounds(&mut self, point: Point) {
|
|
self.size.x.set_max(point.x);
|
|
self.size.y.set_max(point.y);
|
|
}
|
|
|
|
/// Resolve the point relative to the region.
|
|
fn resolve_point(&self, point: Axes<Rel>, relative: bool) -> Point {
|
|
let mut p = point
|
|
.resolve(self.styles)
|
|
.zip_map(self.region.size, Rel::relative_to)
|
|
.to_point();
|
|
if relative {
|
|
p += self.last_point;
|
|
}
|
|
p
|
|
}
|
|
}
|
|
|
|
/// Convert a cubic control point into a quadratic one.
|
|
fn control_c2q(p: Point, c: Point) -> Point {
|
|
1.5 * c - 0.5 * p
|
|
}
|
|
|
|
/// Convert a quadratic control point into a cubic one.
|
|
fn control_q2c(p: Point, c: Point) -> Point {
|
|
(p + 2.0 * c) / 3.0
|
|
}
|
|
|
|
/// Mirror a control point.
|
|
fn mirror_c(p: Point, c: Point) -> Point {
|
|
2.0 * p - c
|
|
}
|
|
|
|
/// Convert a point to a `kurbo::Point`.
|
|
fn point_to_kurbo(point: Point) -> kurbo::Point {
|
|
kurbo::Point::new(point.x.to_raw(), point.y.to_raw())
|
|
}
|
|
|
|
/// Layout the polygon.
|
|
#[typst_macros::time(span = elem.span())]
|
|
pub fn layout_polygon(
|
|
elem: &Packed<PolygonElem>,
|
|
_: &mut Engine,
|
|
_: Locator,
|
|
styles: StyleChain,
|
|
region: Region,
|
|
) -> SourceResult<Frame> {
|
|
let points: Vec<Point> = elem
|
|
.vertices
|
|
.iter()
|
|
.map(|c| c.resolve(styles).zip_map(region.size, Rel::relative_to).to_point())
|
|
.collect();
|
|
|
|
let size = points.iter().fold(Point::zero(), |max, c| c.max(max)).to_size();
|
|
if !size.is_finite() {
|
|
bail!(elem.span(), "cannot create polygon with infinite size");
|
|
}
|
|
|
|
let mut frame = Frame::hard(size);
|
|
|
|
// Only create a curve if there are more than zero points.
|
|
if points.is_empty() {
|
|
return Ok(frame);
|
|
}
|
|
|
|
// Prepare fill and stroke.
|
|
let fill = elem.fill(styles);
|
|
let fill_rule = elem.fill_rule(styles);
|
|
let stroke = match elem.stroke(styles) {
|
|
Smart::Auto if fill.is_none() => Some(FixedStroke::default()),
|
|
Smart::Auto => None,
|
|
Smart::Custom(stroke) => stroke.map(Stroke::unwrap_or_default),
|
|
};
|
|
|
|
// Construct a closed curve given all points.
|
|
let mut curve = Curve::new();
|
|
curve.move_(points[0]);
|
|
for &point in &points[1..] {
|
|
curve.line(point);
|
|
}
|
|
curve.close();
|
|
|
|
let shape = Shape {
|
|
geometry: Geometry::Curve(curve),
|
|
stroke,
|
|
fill,
|
|
fill_rule,
|
|
};
|
|
frame.push(Point::zero(), FrameItem::Shape(shape, elem.span()));
|
|
Ok(frame)
|
|
}
|
|
|
|
/// Lay out the rectangle.
|
|
#[typst_macros::time(span = elem.span())]
|
|
pub fn layout_rect(
|
|
elem: &Packed<RectElem>,
|
|
engine: &mut Engine,
|
|
locator: Locator,
|
|
styles: StyleChain,
|
|
region: Region,
|
|
) -> SourceResult<Frame> {
|
|
layout_shape(
|
|
engine,
|
|
locator,
|
|
styles,
|
|
region,
|
|
ShapeKind::Rect,
|
|
elem.body(styles),
|
|
elem.fill(styles),
|
|
elem.stroke(styles),
|
|
elem.inset(styles),
|
|
elem.outset(styles),
|
|
elem.radius(styles),
|
|
elem.span(),
|
|
)
|
|
}
|
|
|
|
/// Lay out the square.
|
|
#[typst_macros::time(span = elem.span())]
|
|
pub fn layout_square(
|
|
elem: &Packed<SquareElem>,
|
|
engine: &mut Engine,
|
|
locator: Locator,
|
|
styles: StyleChain,
|
|
region: Region,
|
|
) -> SourceResult<Frame> {
|
|
layout_shape(
|
|
engine,
|
|
locator,
|
|
styles,
|
|
region,
|
|
ShapeKind::Square,
|
|
elem.body(styles),
|
|
elem.fill(styles),
|
|
elem.stroke(styles),
|
|
elem.inset(styles),
|
|
elem.outset(styles),
|
|
elem.radius(styles),
|
|
elem.span(),
|
|
)
|
|
}
|
|
|
|
/// Lay out the ellipse.
|
|
#[typst_macros::time(span = elem.span())]
|
|
pub fn layout_ellipse(
|
|
elem: &Packed<EllipseElem>,
|
|
engine: &mut Engine,
|
|
locator: Locator,
|
|
styles: StyleChain,
|
|
region: Region,
|
|
) -> SourceResult<Frame> {
|
|
layout_shape(
|
|
engine,
|
|
locator,
|
|
styles,
|
|
region,
|
|
ShapeKind::Ellipse,
|
|
elem.body(styles),
|
|
elem.fill(styles),
|
|
elem.stroke(styles).map(|s| Sides::splat(Some(s))),
|
|
elem.inset(styles),
|
|
elem.outset(styles),
|
|
Corners::splat(None),
|
|
elem.span(),
|
|
)
|
|
}
|
|
|
|
/// Lay out the circle.
|
|
#[typst_macros::time(span = elem.span())]
|
|
pub fn layout_circle(
|
|
elem: &Packed<CircleElem>,
|
|
engine: &mut Engine,
|
|
locator: Locator,
|
|
styles: StyleChain,
|
|
region: Region,
|
|
) -> SourceResult<Frame> {
|
|
layout_shape(
|
|
engine,
|
|
locator,
|
|
styles,
|
|
region,
|
|
ShapeKind::Circle,
|
|
elem.body(styles),
|
|
elem.fill(styles),
|
|
elem.stroke(styles).map(|s| Sides::splat(Some(s))),
|
|
elem.inset(styles),
|
|
elem.outset(styles),
|
|
Corners::splat(None),
|
|
elem.span(),
|
|
)
|
|
}
|
|
|
|
/// A category of shape.
|
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
|
enum ShapeKind {
|
|
/// A rectangle with equal side lengths.
|
|
Square,
|
|
/// A quadrilateral with four right angles.
|
|
Rect,
|
|
/// An ellipse with coinciding foci.
|
|
Circle,
|
|
/// A curve around two focal points.
|
|
Ellipse,
|
|
}
|
|
|
|
impl ShapeKind {
|
|
/// Whether this shape kind is curvy.
|
|
fn is_round(self) -> bool {
|
|
matches!(self, Self::Circle | Self::Ellipse)
|
|
}
|
|
|
|
/// Whether this shape kind has equal side length.
|
|
fn is_quadratic(self) -> bool {
|
|
matches!(self, Self::Square | Self::Circle)
|
|
}
|
|
}
|
|
|
|
/// Layout a shape.
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn layout_shape(
|
|
engine: &mut Engine,
|
|
locator: Locator,
|
|
styles: StyleChain,
|
|
region: Region,
|
|
kind: ShapeKind,
|
|
body: &Option<Content>,
|
|
fill: Option<Paint>,
|
|
stroke: Smart<Sides<Option<Option<Stroke<Abs>>>>>,
|
|
inset: Sides<Option<Rel<Abs>>>,
|
|
outset: Sides<Option<Rel<Abs>>>,
|
|
radius: Corners<Option<Rel<Abs>>>,
|
|
span: Span,
|
|
) -> SourceResult<Frame> {
|
|
let mut frame;
|
|
if let Some(child) = body {
|
|
let mut inset = inset.unwrap_or_default();
|
|
if kind.is_round() {
|
|
// Apply extra inset to round shapes.
|
|
inset = inset.map(|v| v + Ratio::new(0.5 - SQRT_2 / 4.0));
|
|
}
|
|
let has_inset = !inset.is_zero();
|
|
|
|
// Take the inset, if any, into account.
|
|
let mut pod = region;
|
|
if has_inset {
|
|
pod.size = crate::pad::shrink(region.size, &inset);
|
|
}
|
|
|
|
// If the shape is quadratic, we first measure it to determine its size
|
|
// and then layout with full expansion to force the aspect ratio and
|
|
// make sure it's really quadratic.
|
|
if kind.is_quadratic() {
|
|
let length = match quadratic_size(pod) {
|
|
Some(length) => length,
|
|
None => {
|
|
// Take as much as the child wants, but without overflowing.
|
|
crate::layout_frame(engine, child, locator.relayout(), styles, pod)?
|
|
.size()
|
|
.max_by_side()
|
|
.min(pod.size.min_by_side())
|
|
}
|
|
};
|
|
|
|
pod = Region::new(Size::splat(length), Axes::splat(true));
|
|
}
|
|
|
|
// Layout the child.
|
|
frame = crate::layout_frame(engine, child, locator, styles, pod)?;
|
|
|
|
// Apply the inset.
|
|
if has_inset {
|
|
crate::pad::grow(&mut frame, &inset);
|
|
}
|
|
} else {
|
|
// The default size that a shape takes on if it has no child and no
|
|
// forced sizes.
|
|
let default = Size::new(Abs::pt(45.0), Abs::pt(30.0)).min(region.size);
|
|
|
|
let size = if kind.is_quadratic() {
|
|
Size::splat(match quadratic_size(region) {
|
|
Some(length) => length,
|
|
None => default.min_by_side(),
|
|
})
|
|
} else {
|
|
// For each dimension, pick the region size if forced, otherwise
|
|
// use the default size (or the region size if the default
|
|
// is too large for the region).
|
|
region.expand.select(region.size, default)
|
|
};
|
|
|
|
frame = Frame::soft(size);
|
|
}
|
|
|
|
// Prepare stroke.
|
|
let stroke = match stroke {
|
|
Smart::Auto if fill.is_none() => Sides::splat(Some(FixedStroke::default())),
|
|
Smart::Auto => Sides::splat(None),
|
|
Smart::Custom(strokes) => {
|
|
strokes.unwrap_or_default().map(|s| s.map(Stroke::unwrap_or_default))
|
|
}
|
|
};
|
|
|
|
// Add fill and/or stroke.
|
|
if fill.is_some() || stroke.iter().any(Option::is_some) {
|
|
if kind.is_round() {
|
|
let outset = outset.unwrap_or_default().relative_to(frame.size());
|
|
let size = frame.size() + outset.sum_by_axis();
|
|
let pos = Point::new(-outset.left, -outset.top);
|
|
let shape = Shape {
|
|
geometry: Geometry::Curve(Curve::ellipse(size)),
|
|
fill,
|
|
stroke: stroke.left,
|
|
fill_rule: FillRule::default(),
|
|
};
|
|
frame.prepend(pos, FrameItem::Shape(shape, span));
|
|
} else {
|
|
fill_and_stroke(
|
|
&mut frame,
|
|
fill,
|
|
&stroke,
|
|
&outset.unwrap_or_default(),
|
|
&radius.unwrap_or_default(),
|
|
span,
|
|
);
|
|
}
|
|
}
|
|
|
|
Ok(frame)
|
|
}
|
|
|
|
/// Determines the forced size of a quadratic shape based on the region, if any.
|
|
///
|
|
/// The size is forced if at least one axis is expanded because `expand` is
|
|
/// `true` for axes whose size was manually specified by the user.
|
|
fn quadratic_size(region: Region) -> Option<Abs> {
|
|
if region.expand.x && region.expand.y {
|
|
// If both `width` and `height` are specified, we choose the
|
|
// smaller one.
|
|
Some(region.size.x.min(region.size.y))
|
|
} else if region.expand.x {
|
|
Some(region.size.x)
|
|
} else if region.expand.y {
|
|
Some(region.size.y)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
/// Creates a new rectangle as a curve.
|
|
pub fn clip_rect(
|
|
size: Size,
|
|
radius: &Corners<Rel<Abs>>,
|
|
stroke: &Sides<Option<FixedStroke>>,
|
|
outset: &Sides<Rel<Abs>>,
|
|
) -> Curve {
|
|
let outset = outset.relative_to(size);
|
|
let size = size + outset.sum_by_axis();
|
|
|
|
let stroke_widths = stroke
|
|
.as_ref()
|
|
.map(|s| s.as_ref().map_or(Abs::zero(), |s| s.thickness / 2.0));
|
|
|
|
let max_radius = (size.x.min(size.y)) / 2.0
|
|
+ stroke_widths.iter().cloned().min().unwrap_or(Abs::zero());
|
|
|
|
let radius = radius.map(|side| side.relative_to(max_radius * 2.0).min(max_radius));
|
|
let corners = corners_control_points(size, &radius, stroke, &stroke_widths);
|
|
|
|
let mut curve = Curve::new();
|
|
if corners.top_left.arc_inner() {
|
|
curve.arc_move(
|
|
corners.top_left.start_inner(),
|
|
corners.top_left.center_inner(),
|
|
corners.top_left.end_inner(),
|
|
);
|
|
} else {
|
|
curve.move_(corners.top_left.center_inner());
|
|
}
|
|
for corner in [&corners.top_right, &corners.bottom_right, &corners.bottom_left] {
|
|
if corner.arc_inner() {
|
|
curve.arc_line(
|
|
corner.start_inner(),
|
|
corner.center_inner(),
|
|
corner.end_inner(),
|
|
)
|
|
} else {
|
|
curve.line(corner.center_inner());
|
|
}
|
|
}
|
|
curve.close();
|
|
curve.translate(Point::new(-outset.left, -outset.top));
|
|
curve
|
|
}
|
|
|
|
/// Add a fill and stroke with optional radius and outset to the frame.
|
|
pub fn fill_and_stroke(
|
|
frame: &mut Frame,
|
|
fill: Option<Paint>,
|
|
stroke: &Sides<Option<FixedStroke>>,
|
|
outset: &Sides<Rel<Abs>>,
|
|
radius: &Corners<Rel<Abs>>,
|
|
span: Span,
|
|
) {
|
|
let outset = outset.relative_to(frame.size());
|
|
let size = frame.size() + outset.sum_by_axis();
|
|
let pos = Point::new(-outset.left, -outset.top);
|
|
frame.prepend_multiple(
|
|
styled_rect(size, radius, fill, stroke)
|
|
.into_iter()
|
|
.map(|x| (pos, FrameItem::Shape(x, span))),
|
|
);
|
|
}
|
|
|
|
/// Create a styled rectangle with shapes.
|
|
/// - use rect primitive for simple rectangles
|
|
/// - stroke sides if possible
|
|
/// - use fill for sides for best looks
|
|
pub fn styled_rect(
|
|
size: Size,
|
|
radius: &Corners<Rel<Abs>>,
|
|
fill: Option<Paint>,
|
|
stroke: &Sides<Option<FixedStroke>>,
|
|
) -> Vec<Shape> {
|
|
if stroke.is_uniform() && radius.iter().cloned().all(Rel::is_zero) {
|
|
simple_rect(size, fill, stroke.top.clone())
|
|
} else {
|
|
segmented_rect(size, radius, fill, stroke)
|
|
}
|
|
}
|
|
|
|
/// Use rect primitive for the rectangle
|
|
fn simple_rect(
|
|
size: Size,
|
|
fill: Option<Paint>,
|
|
stroke: Option<FixedStroke>,
|
|
) -> Vec<Shape> {
|
|
vec![Shape {
|
|
geometry: Geometry::Rect(size),
|
|
fill,
|
|
stroke,
|
|
fill_rule: FillRule::default(),
|
|
}]
|
|
}
|
|
|
|
fn corners_control_points(
|
|
size: Size,
|
|
radius: &Corners<Abs>,
|
|
strokes: &Sides<Option<FixedStroke>>,
|
|
stroke_widths: &Sides<Abs>,
|
|
) -> Corners<ControlPoints> {
|
|
Corners {
|
|
top_left: Corner::TopLeft,
|
|
top_right: Corner::TopRight,
|
|
bottom_right: Corner::BottomRight,
|
|
bottom_left: Corner::BottomLeft,
|
|
}
|
|
.map(|corner| ControlPoints {
|
|
radius: radius.get(corner),
|
|
stroke_before: stroke_widths.get(corner.side_ccw()),
|
|
stroke_after: stroke_widths.get(corner.side_cw()),
|
|
corner,
|
|
size,
|
|
same: match (
|
|
strokes.get_ref(corner.side_ccw()),
|
|
strokes.get_ref(corner.side_cw()),
|
|
) {
|
|
(Some(a), Some(b)) => a.paint == b.paint && a.dash == b.dash,
|
|
(None, None) => true,
|
|
_ => false,
|
|
},
|
|
})
|
|
}
|
|
|
|
/// Use stroke and fill for the rectangle
|
|
fn segmented_rect(
|
|
size: Size,
|
|
radius: &Corners<Rel<Abs>>,
|
|
fill: Option<Paint>,
|
|
strokes: &Sides<Option<FixedStroke>>,
|
|
) -> Vec<Shape> {
|
|
let mut res = vec![];
|
|
let stroke_widths = strokes
|
|
.as_ref()
|
|
.map(|s| s.as_ref().map_or(Abs::zero(), |s| s.thickness / 2.0));
|
|
|
|
let max_radius = (size.x.min(size.y)) / 2.0
|
|
+ stroke_widths.iter().cloned().min().unwrap_or(Abs::zero());
|
|
|
|
let radius = radius.map(|side| side.relative_to(max_radius * 2.0).min(max_radius));
|
|
let corners = corners_control_points(size, &radius, strokes, &stroke_widths);
|
|
|
|
// insert stroked sides below filled sides
|
|
let mut stroke_insert = 0;
|
|
|
|
// fill shape with inner curve
|
|
if let Some(fill) = fill {
|
|
let mut curve = Curve::new();
|
|
let c = corners.get_ref(Corner::TopLeft);
|
|
if c.arc() {
|
|
curve.arc_move(c.start(), c.center(), c.end());
|
|
} else {
|
|
curve.move_(c.center());
|
|
};
|
|
|
|
for corner in [Corner::TopRight, Corner::BottomRight, Corner::BottomLeft] {
|
|
let c = corners.get_ref(corner);
|
|
if c.arc() {
|
|
curve.arc_line(c.start(), c.center(), c.end());
|
|
} else {
|
|
curve.line(c.center());
|
|
}
|
|
}
|
|
curve.close();
|
|
res.push(Shape {
|
|
geometry: Geometry::Curve(curve),
|
|
fill: Some(fill),
|
|
fill_rule: FillRule::default(),
|
|
stroke: None,
|
|
});
|
|
stroke_insert += 1;
|
|
}
|
|
|
|
let current = corners.iter().find(|c| !c.same).map(|c| c.corner);
|
|
if let Some(mut current) = current {
|
|
// multiple segments
|
|
// start at a corner with a change between sides and iterate clockwise all other corners
|
|
let mut last = current;
|
|
for _ in 0..4 {
|
|
current = current.next_cw();
|
|
if corners.get_ref(current).same {
|
|
continue;
|
|
}
|
|
// create segment
|
|
let start = last;
|
|
let end = current;
|
|
last = current;
|
|
let Some(stroke) = strokes.get_ref(start.side_cw()) else { continue };
|
|
let (shape, ontop) = segment(start, end, &corners, stroke);
|
|
if ontop {
|
|
res.push(shape);
|
|
} else {
|
|
res.insert(stroke_insert, shape);
|
|
stroke_insert += 1;
|
|
}
|
|
}
|
|
} else if let Some(stroke) = &strokes.top {
|
|
// single segment
|
|
let (shape, _) = segment(Corner::TopLeft, Corner::TopLeft, &corners, stroke);
|
|
res.push(shape);
|
|
}
|
|
res
|
|
}
|
|
|
|
fn curve_segment(
|
|
start: Corner,
|
|
end: Corner,
|
|
corners: &Corners<ControlPoints>,
|
|
curve: &mut Curve,
|
|
) {
|
|
// create start corner
|
|
let c = corners.get_ref(start);
|
|
if start == end || !c.arc() {
|
|
curve.move_(c.end());
|
|
} else {
|
|
curve.arc_move(c.mid(), c.center(), c.end());
|
|
}
|
|
|
|
// create corners between start and end
|
|
let mut current = start.next_cw();
|
|
while current != end {
|
|
let c = corners.get_ref(current);
|
|
if c.arc() {
|
|
curve.arc_line(c.start(), c.center(), c.end());
|
|
} else {
|
|
curve.line(c.end());
|
|
}
|
|
current = current.next_cw();
|
|
}
|
|
|
|
// create end corner
|
|
let c = corners.get_ref(end);
|
|
if !c.arc() {
|
|
curve.line(c.start());
|
|
} else if start == end {
|
|
curve.arc_line(c.start(), c.center(), c.end());
|
|
} else {
|
|
curve.arc_line(c.start(), c.center(), c.mid());
|
|
}
|
|
}
|
|
|
|
/// Returns the shape for the segment and whether the shape should be drawn on top.
|
|
fn segment(
|
|
start: Corner,
|
|
end: Corner,
|
|
corners: &Corners<ControlPoints>,
|
|
stroke: &FixedStroke,
|
|
) -> (Shape, bool) {
|
|
fn fill_corner(corner: &ControlPoints) -> bool {
|
|
corner.stroke_before != corner.stroke_after
|
|
|| corner.radius() < corner.stroke_before
|
|
}
|
|
|
|
fn fill_corners(
|
|
start: Corner,
|
|
end: Corner,
|
|
corners: &Corners<ControlPoints>,
|
|
) -> bool {
|
|
if fill_corner(corners.get_ref(start)) {
|
|
return true;
|
|
}
|
|
if fill_corner(corners.get_ref(end)) {
|
|
return true;
|
|
}
|
|
let mut current = start.next_cw();
|
|
while current != end {
|
|
if fill_corner(corners.get_ref(current)) {
|
|
return true;
|
|
}
|
|
current = current.next_cw();
|
|
}
|
|
false
|
|
}
|
|
|
|
let solid = stroke.dash.as_ref().map(|dash| dash.array.is_empty()).unwrap_or(true);
|
|
|
|
let use_fill = solid && fill_corners(start, end, corners);
|
|
let shape = if use_fill {
|
|
fill_segment(start, end, corners, stroke)
|
|
} else {
|
|
stroke_segment(start, end, corners, stroke.clone())
|
|
};
|
|
|
|
(shape, use_fill)
|
|
}
|
|
|
|
/// Stroke the sides from `start` to `end` clockwise.
|
|
fn stroke_segment(
|
|
start: Corner,
|
|
end: Corner,
|
|
corners: &Corners<ControlPoints>,
|
|
stroke: FixedStroke,
|
|
) -> Shape {
|
|
// Create start corner.
|
|
let mut curve = Curve::new();
|
|
curve_segment(start, end, corners, &mut curve);
|
|
|
|
Shape {
|
|
geometry: Geometry::Curve(curve),
|
|
stroke: Some(stroke),
|
|
fill: None,
|
|
fill_rule: FillRule::default(),
|
|
}
|
|
}
|
|
|
|
/// Fill the sides from `start` to `end` clockwise.
|
|
fn fill_segment(
|
|
start: Corner,
|
|
end: Corner,
|
|
corners: &Corners<ControlPoints>,
|
|
stroke: &FixedStroke,
|
|
) -> Shape {
|
|
let mut curve = Curve::new();
|
|
|
|
// create the start corner
|
|
// begin on the inside and finish on the outside
|
|
// no corner if start and end are equal
|
|
// half corner if different
|
|
if start == end {
|
|
let c = corners.get_ref(start);
|
|
curve.move_(c.end_inner());
|
|
curve.line(c.end_outer());
|
|
} else {
|
|
let c = corners.get_ref(start);
|
|
|
|
if c.arc_inner() {
|
|
curve.arc_move(c.end_inner(), c.center_inner(), c.mid_inner());
|
|
} else {
|
|
curve.move_(c.end_inner());
|
|
}
|
|
|
|
if c.arc_outer() {
|
|
curve.arc_line(c.mid_outer(), c.center_outer(), c.end_outer());
|
|
} else {
|
|
curve.line(c.outer());
|
|
curve.line(c.end_outer());
|
|
}
|
|
}
|
|
|
|
// create the clockwise outside curve for the corners between start and end
|
|
let mut current = start.next_cw();
|
|
while current != end {
|
|
let c = corners.get_ref(current);
|
|
if c.arc_outer() {
|
|
curve.arc_line(c.start_outer(), c.center_outer(), c.end_outer());
|
|
} else {
|
|
curve.line(c.outer());
|
|
}
|
|
current = current.next_cw();
|
|
}
|
|
|
|
// create the end corner
|
|
// begin on the outside and finish on the inside
|
|
// full corner if start and end are equal
|
|
// half corner if different
|
|
if start == end {
|
|
let c = corners.get_ref(end);
|
|
if c.arc_outer() {
|
|
curve.arc_line(c.start_outer(), c.center_outer(), c.end_outer());
|
|
} else {
|
|
curve.line(c.outer());
|
|
curve.line(c.end_outer());
|
|
}
|
|
if c.arc_inner() {
|
|
curve.arc_line(c.end_inner(), c.center_inner(), c.start_inner());
|
|
} else {
|
|
curve.line(c.center_inner());
|
|
}
|
|
} else {
|
|
let c = corners.get_ref(end);
|
|
if c.arc_outer() {
|
|
curve.arc_line(c.start_outer(), c.center_outer(), c.mid_outer());
|
|
} else {
|
|
curve.line(c.outer());
|
|
}
|
|
if c.arc_inner() {
|
|
curve.arc_line(c.mid_inner(), c.center_inner(), c.start_inner());
|
|
} else {
|
|
curve.line(c.center_inner());
|
|
}
|
|
}
|
|
|
|
// create the counterclockwise inside curve for the corners between start and end
|
|
let mut current = end.next_ccw();
|
|
while current != start {
|
|
let c = corners.get_ref(current);
|
|
if c.arc_inner() {
|
|
curve.arc_line(c.end_inner(), c.center_inner(), c.start_inner());
|
|
} else {
|
|
curve.line(c.center_inner());
|
|
}
|
|
current = current.next_ccw();
|
|
}
|
|
|
|
curve.close();
|
|
|
|
Shape {
|
|
geometry: Geometry::Curve(curve),
|
|
stroke: None,
|
|
fill: Some(stroke.paint.clone()),
|
|
fill_rule: FillRule::default(),
|
|
}
|
|
}
|
|
|
|
/// Helper to calculate different control points for the corners.
|
|
/// Clockwise orientation from start to end.
|
|
/// ```text
|
|
/// O-------------------EO --- - Z: Zero/Origin ({x: 0, y: 0} for top left corner)
|
|
/// |\ ___----''' | | - O: Outer: intersection between the straight outer lines
|
|
/// | \ / | | - S_: start
|
|
/// | MO | | - M_: midpoint
|
|
/// | /Z\ __-----------E | - E_: end
|
|
/// |/ \M | ro - r_: radius
|
|
/// | /\ | | - middle of the stroke
|
|
/// | / \ | | - arc from S through M to E with center C and radius r
|
|
/// | | MI--EI------- | - outer curve
|
|
/// | | / \ | - arc from SO through MO to EO with center CO and radius ro
|
|
/// SO | | \ CO --- - inner curve
|
|
/// | | | \ - arc from SI through MI to EI with center CI and radius ri
|
|
/// |--S-SI-----CI C
|
|
/// |--ri--|
|
|
/// |-------r--------|
|
|
/// ```
|
|
struct ControlPoints {
|
|
radius: Abs,
|
|
stroke_after: Abs,
|
|
stroke_before: Abs,
|
|
corner: Corner,
|
|
size: Size,
|
|
same: bool,
|
|
}
|
|
|
|
impl ControlPoints {
|
|
/// Move and rotate the point from top-left to the required corner.
|
|
fn rotate(&self, point: Point) -> Point {
|
|
match self.corner {
|
|
Corner::TopLeft => point,
|
|
Corner::TopRight => Point { x: self.size.x - point.y, y: point.x },
|
|
Corner::BottomRight => {
|
|
Point { x: self.size.x - point.x, y: self.size.y - point.y }
|
|
}
|
|
Corner::BottomLeft => Point { x: point.y, y: self.size.y - point.x },
|
|
}
|
|
}
|
|
|
|
/// Outside intersection of the sides.
|
|
pub fn outer(&self) -> Point {
|
|
self.rotate(Point { x: -self.stroke_before, y: -self.stroke_after })
|
|
}
|
|
|
|
/// Center for the outer arc.
|
|
pub fn center_outer(&self) -> Point {
|
|
let r = self.radius_outer();
|
|
self.rotate(Point {
|
|
x: r - self.stroke_before,
|
|
y: r - self.stroke_after,
|
|
})
|
|
}
|
|
|
|
/// Center for the middle arc.
|
|
pub fn center(&self) -> Point {
|
|
let r = self.radius();
|
|
self.rotate(Point { x: r, y: r })
|
|
}
|
|
|
|
/// Center for the inner arc.
|
|
pub fn center_inner(&self) -> Point {
|
|
let r = self.radius_inner();
|
|
|
|
self.rotate(Point {
|
|
x: self.stroke_before + r,
|
|
y: self.stroke_after + r,
|
|
})
|
|
}
|
|
|
|
/// Radius of the outer arc.
|
|
pub fn radius_outer(&self) -> Abs {
|
|
self.radius
|
|
}
|
|
|
|
/// Radius of the middle arc.
|
|
pub fn radius(&self) -> Abs {
|
|
(self.radius - self.stroke_before.min(self.stroke_after)).max(Abs::zero())
|
|
}
|
|
|
|
/// Radius of the inner arc.
|
|
pub fn radius_inner(&self) -> Abs {
|
|
(self.radius - 2.0 * self.stroke_before.max(self.stroke_after)).max(Abs::zero())
|
|
}
|
|
|
|
/// Middle of the corner on the outside of the stroke.
|
|
pub fn mid_outer(&self) -> Point {
|
|
let c_i = self.center_inner();
|
|
let c_o = self.center_outer();
|
|
let o = self.outer();
|
|
let r = self.radius_outer();
|
|
|
|
// https://math.stackexchange.com/a/311956
|
|
// intersection between the line from inner center to outside and the outer arc
|
|
let a = (o.x - c_i.x).to_raw().powi(2) + (o.y - c_i.y).to_raw().powi(2);
|
|
let b = 2.0 * (o.x - c_i.x).to_raw() * (c_i.x - c_o.x).to_raw()
|
|
+ 2.0 * (o.y - c_i.y).to_raw() * (c_i.y - c_o.y).to_raw();
|
|
let c = (c_i.x - c_o.x).to_raw().powi(2) + (c_i.y - c_o.y).to_raw().powi(2)
|
|
- r.to_raw().powi(2);
|
|
let t = (-b + (b * b - 4.0 * a * c).max(0.0).sqrt()) / (2.0 * a);
|
|
c_i + t * (o - c_i)
|
|
}
|
|
|
|
/// Middle of the corner in the middle of the stroke.
|
|
pub fn mid(&self) -> Point {
|
|
let center = self.center_outer();
|
|
let outer = self.outer();
|
|
let diff = outer - center;
|
|
center + diff / diff.hypot().to_raw() * self.radius().to_raw()
|
|
}
|
|
|
|
/// Middle of the corner on the inside of the stroke.
|
|
pub fn mid_inner(&self) -> Point {
|
|
let center = self.center_inner();
|
|
let outer = self.outer();
|
|
let diff = outer - center;
|
|
center + diff / diff.hypot().to_raw() * self.radius_inner().to_raw()
|
|
}
|
|
|
|
/// If an outer arc is required.
|
|
pub fn arc_outer(&self) -> bool {
|
|
self.radius_outer() > Abs::zero()
|
|
}
|
|
|
|
pub fn arc(&self) -> bool {
|
|
self.radius() > Abs::zero()
|
|
}
|
|
|
|
/// If an inner arc is required.
|
|
pub fn arc_inner(&self) -> bool {
|
|
self.radius_inner() > Abs::zero()
|
|
}
|
|
|
|
/// Start of the corner on the outside of the stroke.
|
|
pub fn start_outer(&self) -> Point {
|
|
self.rotate(Point {
|
|
x: -self.stroke_before,
|
|
y: self.radius_outer() - self.stroke_after,
|
|
})
|
|
}
|
|
|
|
/// Start of the corner in the center of the stroke.
|
|
pub fn start(&self) -> Point {
|
|
self.rotate(Point::with_y(self.radius()))
|
|
}
|
|
|
|
/// Start of the corner on the inside of the stroke.
|
|
pub fn start_inner(&self) -> Point {
|
|
self.rotate(Point {
|
|
x: self.stroke_before,
|
|
y: self.stroke_after + self.radius_inner(),
|
|
})
|
|
}
|
|
|
|
/// End of the corner on the outside of the stroke.
|
|
pub fn end_outer(&self) -> Point {
|
|
self.rotate(Point {
|
|
x: self.radius_outer() - self.stroke_before,
|
|
y: -self.stroke_after,
|
|
})
|
|
}
|
|
|
|
/// End of the corner in the center of the stroke.
|
|
pub fn end(&self) -> Point {
|
|
self.rotate(Point::with_x(self.radius()))
|
|
}
|
|
|
|
/// End of the corner on the inside of the stroke.
|
|
pub fn end_inner(&self) -> Point {
|
|
self.rotate(Point {
|
|
x: self.stroke_before + self.radius_inner(),
|
|
y: self.stroke_after,
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Helper to draw arcs with bezier curves.
|
|
trait CurveExt {
|
|
fn arc(&mut self, start: Point, center: Point, end: Point);
|
|
fn arc_move(&mut self, start: Point, center: Point, end: Point);
|
|
fn arc_line(&mut self, start: Point, center: Point, end: Point);
|
|
}
|
|
|
|
impl CurveExt for Curve {
|
|
fn arc(&mut self, start: Point, center: Point, end: Point) {
|
|
let arc = bezier_arc_control(start, center, end);
|
|
self.cubic(arc[0], arc[1], end);
|
|
}
|
|
|
|
fn arc_move(&mut self, start: Point, center: Point, end: Point) {
|
|
self.move_(start);
|
|
self.arc(start, center, end);
|
|
}
|
|
|
|
fn arc_line(&mut self, start: Point, center: Point, end: Point) {
|
|
self.line(start);
|
|
self.arc(start, center, end);
|
|
}
|
|
}
|
|
|
|
/// Get the control points for a bezier curve that approximates a circular arc for
|
|
/// a start point, an end point and a center of the circle whose arc connects
|
|
/// the two.
|
|
fn bezier_arc_control(start: Point, center: Point, end: Point) -> [Point; 2] {
|
|
// https://stackoverflow.com/a/44829356/1567835
|
|
let a = start - center;
|
|
let b = end - center;
|
|
|
|
let q1 = a.x.to_raw() * a.x.to_raw() + a.y.to_raw() * a.y.to_raw();
|
|
let q2 = q1 + a.x.to_raw() * b.x.to_raw() + a.y.to_raw() * b.y.to_raw();
|
|
let k2 = (4.0 / 3.0) * ((2.0 * q1 * q2).sqrt() - q2)
|
|
/ (a.x.to_raw() * b.y.to_raw() - a.y.to_raw() * b.x.to_raw());
|
|
|
|
let control_1 = Point::new(center.x + a.x - k2 * a.y, center.y + a.y + k2 * a.x);
|
|
let control_2 = Point::new(center.x + b.x + k2 * b.y, center.y + b.y - k2 * b.x);
|
|
|
|
[control_1, control_2]
|
|
}
|