// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
/// Path builder sink interface.
///
/// Ported from upstream `zeno/src/path_builder.rs` (Apache-2.0 OR MIT).
pub(open) trait PathBuilder {
/// Returns the current point of the path.
current_point(Self) -> Point
move_to(Self, Point) -> Unit
line_to(Self, Point) -> Unit
quad_to(Self, Point, Point) -> Unit
curve_to(Self, Point, Point, Point) -> Unit
close(Self) -> Unit
}
///|
/// Convenience operations built on top of the core `PathBuilder` methods.
///
/// Upstream Rust exposes these as default trait methods on `PathBuilder`.
/// MoonBit traits do not support default method bodies, so we provide them as
/// free functions.
pub fn rel_move_to(sink : &PathBuilder, to : Point) -> Unit {
PathBuilder::move_to(sink, to + PathBuilder::current_point(sink))
}
///|
pub fn rel_line_to(sink : &PathBuilder, to : Point) -> Unit {
PathBuilder::line_to(sink, to + PathBuilder::current_point(sink))
}
///|
pub fn rel_quad_to(sink : &PathBuilder, c : Point, to : Point) -> Unit {
let r = PathBuilder::current_point(sink)
PathBuilder::quad_to(sink, c + r, to + r)
}
///|
pub fn rel_curve_to(
sink : &PathBuilder,
c1 : Point,
c2 : Point,
to : Point,
) -> Unit {
let r = PathBuilder::current_point(sink)
PathBuilder::curve_to(sink, c1 + r, c2 + r, to + r)
}
///|
pub fn arc_to(
sink : &PathBuilder,
rx : Double,
ry : Double,
angle : Angle,
size : ArcSize,
sweep : ArcSweep,
to : Point,
) -> Unit {
let from = PathBuilder::current_point(sink)
arc(sink, from, rx, ry, angle.to_radians(), size, sweep, to)
}
///|
pub fn rel_arc_to(
sink : &PathBuilder,
rx : Double,
ry : Double,
angle : Angle,
size : ArcSize,
sweep : ArcSweep,
to : Point,
) -> Unit {
arc_to(
sink,
rx,
ry,
angle,
size,
sweep,
to + PathBuilder::current_point(sink),
)
}
///|
pub fn add_rect(
sink : &PathBuilder,
xy : Point,
w : Double,
h : Double,
) -> Unit {
let p = xy
let l = p.x()
let t = p.y()
let r = l + w
let b = t + h
PathBuilder::move_to(sink, p)
PathBuilder::line_to(sink, Vector(r, t))
PathBuilder::line_to(sink, Vector(r, b))
PathBuilder::line_to(sink, Vector(l, b))
PathBuilder::close(sink)
}
///|
pub fn add_round_rect(
sink : &PathBuilder,
xy : Point,
w : Double,
h : Double,
rx0 : Double,
ry0 : Double,
) -> Unit {
let p = xy
let size = ArcSize::Small
let sweep = ArcSweep::Positive
let a = Angle::from_radians(0.0)
let hw = w * 0.5
let rx = if rx0 < 0.0 { 0.0 } else if rx0 > hw { hw } else { rx0 }
let hh = h * 0.5
let ry = if ry0 < 0.0 { 0.0 } else if ry0 > hh { hh } else { ry0 }
PathBuilder::move_to(sink, Vector(p.x() + rx, p.y()))
PathBuilder::line_to(sink, Vector(p.x() + w - rx, p.y()))
arc_to(sink, rx, ry, a, size, sweep, Vector(p.x() + w, p.y() + ry))
PathBuilder::line_to(sink, Vector(p.x() + w, p.y() + h - ry))
arc_to(sink, rx, ry, a, size, sweep, Vector(p.x() + w - rx, p.y() + h))
PathBuilder::line_to(sink, Vector(p.x() + rx, p.y() + h))
arc_to(sink, rx, ry, a, size, sweep, Vector(p.x(), p.y() + h - ry))
PathBuilder::line_to(sink, Vector(p.x(), p.y() + ry))
arc_to(sink, rx, ry, a, size, sweep, Vector(p.x() + rx, p.y()))
PathBuilder::close(sink)
}
///|
pub fn add_ellipse(
sink : &PathBuilder,
center : Point,
rx : Double,
ry : Double,
) -> Unit {
let cx = center.x()
let cy = center.y()
let a = 0.551915024494
let arx = a * rx
let ary = a * ry
PathBuilder::move_to(sink, Vector(cx + rx, cy))
PathBuilder::curve_to(
sink,
Vector(cx + rx, cy + ary),
Vector(cx + arx, cy + ry),
Vector(cx, cy + ry),
)
PathBuilder::curve_to(
sink,
Vector(cx - arx, cy + ry),
Vector(cx - rx, cy + ary),
Vector(cx - rx, cy),
)
PathBuilder::curve_to(
sink,
Vector(cx - rx, cy - ary),
Vector(cx - arx, cy - ry),
Vector(cx, cy - ry),
)
PathBuilder::curve_to(
sink,
Vector(cx + arx, cy - ry),
Vector(cx + rx, cy - ary),
Vector(cx + rx, cy),
)
PathBuilder::close(sink)
}
///|
pub fn add_circle(sink : &PathBuilder, center : Point, r : Double) -> Unit {
add_ellipse(sink, center, r, r)
}
// NOTE: trig functions are provided by `moonbitlang/core/math` (@coremath).
///|
/// Arc size flag (SVG style).
///
/// Ported from upstream `zeno/src/path_builder.rs`.
pub(all) enum ArcSize {
Small
Large
}
///|
/// Arc sweep flag (SVG style).
///
/// Ported from upstream `zeno/src/path_builder.rs`.
pub(all) enum ArcSweep {
Positive
Negative
}
///|
fn acos(x : Double) -> Double {
@coremath.acos(x)
}
///|
fn sin(x : Double) -> Double {
@coremath.sin(x)
}
///|
fn cos(x : Double) -> Double {
@coremath.cos(x)
}
///|
/// Iterator that generates cubic Beziers for an arc.
///
/// Ported from upstream `zeno/src/path_builder.rs::Arc`.
priv struct Arc {
mut count : Int
center : (Double, Double)
radii : (Double, Double)
cosphi : Double
sinphi : Double
mut ang1 : Double
ang2 : Double
a : Double
}
///|
fn Arc::default() -> Arc {
{
count: 0,
center: (0.0, 0.0),
radii: (0.0, 0.0),
cosphi: 1.0,
sinphi: 0.0,
ang1: 0.0,
ang2: 0.0,
a: 0.0,
}
}
///|
fn Arc::Arc(
from : Point,
rx : Double,
ry : Double,
angle : Double,
size : ArcSize,
sweep : ArcSweep,
to : Point,
) -> Arc {
let px = from.x()
let py = from.y()
// NOTE: Keep upstream's slightly imprecise TAU constant.
let tau = 3.141579 * 2.0
let sinphi = sin(angle)
let cosphi = cos(angle)
let pxp = cosphi * (px - to.x()) / 2.0 + sinphi * (py - to.y()) / 2.0
let pyp = -sinphi * (px - to.x()) / 2.0 + cosphi * (py - to.y()) / 2.0
if pxp == 0.0 && pyp == 0.0 {
return Arc::default()
}
let mut rx0 = rx.abs()
let mut ry0 = ry.abs()
let lambda = pxp * pxp / (rx0 * rx0) + pyp * pyp / (ry0 * ry0)
if lambda > 1.0 {
let s = lambda.sqrt()
rx0 = rx0 * s
ry0 = ry0 * s
}
let large_arc = match size {
Large => true
_ => false
}
let sweep_pos = match sweep {
Positive => true
_ => false
}
fn vec_angle(ux : Double, uy : Double, vx : Double, vy : Double) -> Double {
let sign = if ux * vy - uy * vx < 0.0 { -1.0 } else { 1.0 }
let dot0 = ux * vx + uy * vy
let dot = if dot0 < -1.0 { -1.0 } else if dot0 > 1.0 { 1.0 } else { dot0 }
sign * acos(dot)
}
let rxsq = rx0 * rx0
let rysq = ry0 * ry0
let pxpsq = pxp * pxp
let pypsq = pyp * pyp
let mut radicant = rxsq * rysq - rxsq * pypsq - rysq * pxpsq
if radicant < 0.0 {
radicant = 0.0
}
radicant = radicant / (rxsq * pypsq + rysq * pxpsq)
let sign = if large_arc == sweep_pos { -1.0 } else { 1.0 }
radicant = radicant.sqrt() * sign
let cxp = radicant * rx0 / ry0 * pyp
let cyp = radicant * -ry0 / rx0 * pxp
let cx = cosphi * cxp - sinphi * cyp + (px + to.x()) / 2.0
let cy = sinphi * cxp + cosphi * cyp + (py + to.y()) / 2.0
let vx1 = (pxp - cxp) / rx0
let vy1 = (pyp - cyp) / ry0
let vx2 = (-pxp - cxp) / rx0
let vy2 = (-pyp - cyp) / ry0
let ang1 = vec_angle(1.0, 0.0, vx1, vy1)
let mut ang2 = vec_angle(vx1, vy1, vx2, vy2)
if !sweep_pos && ang2 > 0.0 {
ang2 = ang2 - tau
}
if sweep_pos && ang2 < 0.0 {
ang2 = ang2 + tau
}
let mut ratio = ang2.abs() / (tau / 4.0)
if (1.0 - ratio).abs() < 0.0000001 {
ratio = 1.0
}
let segments = ratio.ceil()
let seg0 = if segments < 1.0 { 1.0 } else { segments }
let count = seg0.to_int()
ang2 = ang2 / seg0
let a = if ang2 == @coremath.PI / 2.0 {
0.551915024494
} else if ang2 == -(@coremath.PI / 2.0) {
-0.551915024494
} else {
4.0 / 3.0 * @coremath.tan(ang2 / 4.0)
}
{ count, center: (cx, cy), radii: (rx0, ry0), sinphi, cosphi, ang1, ang2, a }
}
///|
fn Arc::next(self : Arc) -> Command? {
if self.count <= 0 {
return None
}
self.count = self.count - 1
let y1 = sin(self.ang1)
let x1 = cos(self.ang1)
let y2 = sin(self.ang1 + self.ang2)
let x2 = cos(self.ang1 + self.ang2)
let a = self.a
let (cx, cy) = self.center
let (rx, ry) = self.radii
let sinphi = self.sinphi
let cosphi = self.cosphi
let c1 = Vector((x1 - y1 * a) * rx, (y1 + x1 * a) * ry)
let c1 = Vector(
cx + (cosphi * c1.x() - sinphi * c1.y()),
cy + (sinphi * c1.x() + cosphi * c1.y()),
)
let c2 = Vector((x2 + y2 * a) * rx, (y2 - x2 * a) * ry)
let c2 = Vector(
cx + (cosphi * c2.x() - sinphi * c2.y()),
cy + (sinphi * c2.x() + cosphi * c2.y()),
)
let p2 = Vector(x2 * rx, y2 * ry)
let p2 = Vector(
cx + (cosphi * p2.x() - sinphi * p2.y()),
cy + (sinphi * p2.x() + cosphi * p2.y()),
)
self.ang1 = self.ang1 + self.ang2
Some(CurveTo(c1, c2, p2))
}
///|
/// Emits an elliptical arc as one or more cubic Beziers into the sink.
///
/// Ported from upstream `zeno/src/path_builder.rs::arc`.
fn arc(
sink : &PathBuilder,
from : Point,
rx : Double,
ry : Double,
angle : Double,
size : ArcSize,
sweep : ArcSweep,
to : Point,
) -> Unit {
let p = from
let px = p.x()
let py = p.y()
let tau = @coremath.PI * 2.0
let sinphi = sin(angle)
let cosphi = cos(angle)
let pxp = cosphi * (px - to.x()) / 2.0 + sinphi * (py - to.y()) / 2.0
let pyp = -sinphi * (px - to.x()) / 2.0 + cosphi * (py - to.y()) / 2.0
if pxp == 0.0 && pyp == 0.0 {
return
}
let mut rx0 = rx.abs()
let mut ry0 = ry.abs()
let lambda = pxp * pxp / (rx0 * rx0) + pyp * pyp / (ry0 * ry0)
if lambda > 1.0 {
let s = lambda.sqrt()
rx0 = rx0 * s
ry0 = ry0 * s
}
let large_arc = match size {
Large => true
_ => false
}
let sweep_pos = match sweep {
Positive => true
_ => false
}
fn vec_angle(ux : Double, uy : Double, vx : Double, vy : Double) -> Double {
let sign = if ux * vy - uy * vx < 0.0 { -1.0 } else { 1.0 }
let dot0 = ux * vx + uy * vy
let dot = if dot0 < -1.0 { -1.0 } else if dot0 > 1.0 { 1.0 } else { dot0 }
sign * acos(dot)
}
let rxsq = rx0 * rx0
let rysq = ry0 * ry0
let pxpsq = pxp * pxp
let pypsq = pyp * pyp
let mut radicant = rxsq * rysq - rxsq * pypsq - rysq * pxpsq
if radicant < 0.0 {
radicant = 0.0
}
radicant = radicant / (rxsq * pypsq + rysq * pxpsq)
let sign = if large_arc == sweep_pos { -1.0 } else { 1.0 }
radicant = radicant.sqrt() * sign
let cxp = radicant * rx0 / ry0 * pyp
let cyp = radicant * -ry0 / rx0 * pxp
let cx = cosphi * cxp - sinphi * cyp + (px + to.x()) / 2.0
let cy = sinphi * cxp + cosphi * cyp + (py + to.y()) / 2.0
let vx1 = (pxp - cxp) / rx0
let vy1 = (pyp - cyp) / ry0
let vx2 = (-pxp - cxp) / rx0
let vy2 = (-pyp - cyp) / ry0
let mut ang1 = vec_angle(1.0, 0.0, vx1, vy1)
let mut ang2 = vec_angle(vx1, vy1, vx2, vy2)
if !sweep_pos && ang2 > 0.0 {
ang2 = ang2 - tau
}
if sweep_pos && ang2 < 0.0 {
ang2 = ang2 + tau
}
let mut ratio = ang2.abs() / (tau / 4.0)
if (1.0 - ratio).abs() < 0.0000001 {
ratio = 1.0
}
let segments0 = ratio.ceil()
let segments = if segments0 < 1.0 { 1.0 } else { segments0 }
let seg_count = segments.to_int()
ang2 = ang2 / segments
let a = if ang2 == @coremath.PI / 2.0 {
0.551915024494
} else if ang2 == -(@coremath.PI / 2.0) {
-0.551915024494
} else {
4.0 / 3.0 * @coremath.tan(ang2 / 4.0)
}
for _i in 0..`.
pub impl PathBuilder for Array[Command] with current_point(self) {
let n = self.length()
if n == 0 {
return Vector::zero()
}
match self[n - 1] {
MoveTo(p) => p
LineTo(p) => p
QuadTo(_, p) => p
CurveTo(_, _, p) => p
Close => {
// Find the start point of the current subpath.
if n <= 1 {
return Vector::zero()
}
let mut i = n - 2
while i >= 0 {
match self[i] {
MoveTo(p) => return p
_ => ()
}
if i == 0 {
break
}
i = i - 1
}
Vector::zero()
}
}
}
///|
pub impl PathBuilder for Array[Command] with move_to(self, to) {
self.push(MoveTo(to))
}
///|
pub impl PathBuilder for Array[Command] with line_to(self, to) {
self.push(LineTo(to))
}
///|
pub impl PathBuilder for Array[Command] with quad_to(self, c, to) {
self.push(QuadTo(c, to))
}
///|
pub impl PathBuilder for Array[Command] with curve_to(self, c1, c2, to) {
self.push(CurveTo(c1, c2, to))
}
///|
pub impl PathBuilder for Array[Command] with close(self) {
self.push(Close)
}