// Shape generators for rect, symbol, line and area marks (vega-scenegraph
// path/shapes.js, path/symbols.js, path/rectangle.js and d3-shape).
///|
let half_sqrt3 : Double = 0.8660254037844386
///|
let tan30 : Double = 0.5773502691896257
///|
pub(all) suberror SceneError {
SceneError(String)
}
///|
fn clamp(value : Double, lo : Double, hi : Double) -> Double {
@util.js_max(lo, @util.js_min(value, hi))
}
///|
fn corner(item : Item, key : String) -> Double {
match item.opt_num(key) {
Some(v) => if v != 0.0 && !v.is_nan() { v } else { 0.0 }
None => item.num0("cornerRadius")
}
}
///|
pub fn Item::has_corner_radius(self : Item) -> Bool {
self.truthy("cornerRadius") ||
self.truthy("cornerRadiusTopLeft") ||
self.truthy("cornerRadiusTopRight") ||
self.truthy("cornerRadiusBottomRight") ||
self.truthy("cornerRadiusBottomLeft")
}
///|
/// vega's `rectangle` shape: a plain rectangle or one with rounded corners.
pub fn draw_rect(ctx : Ctx, item : Item, x0? : Double, y0? : Double) -> Unit {
let x1 = match x0 {
Some(v) => v
None => item.num0("x")
}
let y1 = match y0 {
Some(v) => v
None => item.num0("y")
}
let w = item.num0("width")
let h = item.num0("height")
let s = @util.js_min(w, h) / 2.0
let tl = clamp(corner(item, "cornerRadiusTopLeft"), 0.0, s)
let tr = clamp(corner(item, "cornerRadiusTopRight"), 0.0, s)
let br = clamp(corner(item, "cornerRadiusBottomRight"), 0.0, s)
let bl = clamp(corner(item, "cornerRadiusBottomLeft"), 0.0, s)
if tl <= 0.0 && tr <= 0.0 && bl <= 0.0 && br <= 0.0 {
ctx.rect(x1, y1, w, h)
} else {
let c = 0.448084975506
let x2 = x1 + w
let y2 = y1 + h
ctx.move_to(x1 + tl, y1)
ctx.line_to(x2 - tr, y1)
ctx.bezier_to(x2 - c * tr, y1, x2, y1 + c * tr, x2, y1 + tr)
ctx.line_to(x2, y2 - br)
ctx.bezier_to(x2, y2 - c * br, x2 - c * br, y2, x2 - br, y2)
ctx.line_to(x1 + bl, y2)
ctx.bezier_to(x1 + c * bl, y2, x1, y2 - c * bl, x1, y2 - bl)
ctx.line_to(x1, y1 + tl)
ctx.bezier_to(x1, y1 + c * tl, x1 + c * tl, y1, x1 + tl, y1)
ctx.close_path()
}
}
///|
/// The symbol shapes of vega-scenegraph (`path/symbols.js`), drawn at the origin.
pub fn draw_symbol(
ctx : Ctx,
shape : String,
size : Double,
) -> Unit raise SceneError {
match shape {
"circle" => {
let r = size.sqrt() / 2.0
ctx.move_to(r, 0.0)
ctx.arc(0.0, 0.0, r, 0.0, tau, false)
}
"cross" => {
let r = size.sqrt() / 2.0
let s = r / 2.5
ctx.move_to(-r, -s)
ctx.line_to(-r, s)
ctx.line_to(-s, s)
ctx.line_to(-s, r)
ctx.line_to(s, r)
ctx.line_to(s, s)
ctx.line_to(r, s)
ctx.line_to(r, -s)
ctx.line_to(s, -s)
ctx.line_to(s, -r)
ctx.line_to(-s, -r)
ctx.line_to(-s, -s)
ctx.close_path()
}
"diamond" => {
let r = size.sqrt() / 2.0
ctx.move_to(-r, 0.0)
ctx.line_to(0.0, -r)
ctx.line_to(r, 0.0)
ctx.line_to(0.0, r)
ctx.close_path()
}
"square" => {
let w = size.sqrt()
let x = -w / 2.0
ctx.rect(x, x, w, w)
}
"arrow" => {
let r = size.sqrt() / 2.0
let s = r / 7.0
let t = r / 2.5
let v = r / 8.0
ctx.move_to(-s, r)
ctx.line_to(s, r)
ctx.line_to(s, -v)
ctx.line_to(t, -v)
ctx.line_to(0.0, -r)
ctx.line_to(-t, -v)
ctx.line_to(-s, -v)
ctx.close_path()
}
"wedge" => {
let r = size.sqrt() / 2.0
let h = half_sqrt3 * r
let o = h - r * tan30
let b = r / 4.0
ctx.move_to(0.0, -h - o)
ctx.line_to(-b, h - o)
ctx.line_to(b, h - o)
ctx.close_path()
}
"triangle" => {
let r = size.sqrt() / 2.0
let h = half_sqrt3 * r
let o = h - r * tan30
ctx.move_to(0.0, -h - o)
ctx.line_to(-r, h - o)
ctx.line_to(r, h - o)
ctx.close_path()
}
"triangle-up" => {
let r = size.sqrt() / 2.0
let h = half_sqrt3 * r
ctx.move_to(0.0, -h)
ctx.line_to(-r, h)
ctx.line_to(r, h)
ctx.close_path()
}
"triangle-down" => {
let r = size.sqrt() / 2.0
let h = half_sqrt3 * r
ctx.move_to(0.0, h)
ctx.line_to(-r, -h)
ctx.line_to(r, -h)
ctx.close_path()
}
"triangle-right" => {
let r = size.sqrt() / 2.0
let h = half_sqrt3 * r
ctx.move_to(h, 0.0)
ctx.line_to(-h, -r)
ctx.line_to(-h, r)
ctx.close_path()
}
"triangle-left" => {
let r = size.sqrt() / 2.0
let h = half_sqrt3 * r
ctx.move_to(-h, 0.0)
ctx.line_to(h, -r)
ctx.line_to(h, r)
ctx.close_path()
}
"stroke" => {
let r = size.sqrt() / 2.0
ctx.move_to(-r, 0.0)
ctx.line_to(r, 0.0)
}
other => raise SceneError("unsupported symbol shape: " + other)
}
}
///|
/// The symbol of one item, drawn at the origin (d3-shape `symbol()` with
/// `type(item.shape || 'circle')` and `size(item.size ?? 64)`).
pub fn draw_item_symbol(ctx : Ctx, item : Item) -> Unit raise SceneError {
let shape = match item.get("shape") {
v if v.truthy() => v.to_js_string()
_ => "circle"
}
let size = item.num_or("size", 64.0)
draw_symbol(ctx, shape, size)
}
///|
fn defined(item : Item) -> Bool {
match item.get("defined") {
Bool(false) => false
_ => true
}
}
///|
fn make_curve(items : Array[Item]) -> Curve raise SceneError {
let first = items[0]
let interp = match first.get("interpolate") {
v if v.truthy() => v.to_js_string()
_ => "linear"
}
let orient = match first.str("orient") {
Some(o) => o
None => ""
}
match curve_kind(interp, orient) {
Some(k) => Curve::new(k)
None => raise SceneError("unsupported interpolate: " + interp)
}
}
///|
/// d3-shape `line()` over the items (`x`, `y`, `defined`).
pub fn draw_line(ctx : Ctx, items : Array[Item]) -> Unit raise SceneError {
if items.length() == 0 {
return
}
let curve = make_curve(items)
let mut defined0 = false
let n = items.length()
for i = 0; i <= n; i = i + 1 {
let is_def = i < n && defined(items[i])
if is_def != defined0 {
defined0 = !defined0
if defined0 {
curve.line_start(ctx)
} else {
curve.line_end(ctx)
}
}
if defined0 {
curve.point(ctx, items[i].num0("x"), items[i].num0("y"))
}
}
}
///|
/// d3-shape `area()`: vertical areas span `y` to `y + height`, horizontal
/// areas span `x` to `x + width`.
pub fn draw_area(ctx : Ctx, items : Array[Item]) -> Unit raise SceneError {
if items.length() == 0 {
return
}
let curve = make_curve(items)
let horizontal = match items[0].str("orient") {
Some("horizontal") => true
_ => false
}
let n = items.length()
let x0z : Array[Double] = Array::make(n, 0.0)
let y0z : Array[Double] = Array::make(n, 0.0)
let mut defined0 = false
let mut j = 0
for i = 0; i <= n; i = i + 1 {
let is_def = i < n && defined(items[i])
if is_def != defined0 {
defined0 = !defined0
if defined0 {
j = i
curve.area_start()
curve.line_start(ctx)
} else {
curve.line_end(ctx)
curve.line_start(ctx)
let mut k = i - 1
while k >= j {
curve.point(ctx, x0z[k], y0z[k])
k = k - 1
}
curve.line_end(ctx)
curve.area_end()
}
}
if defined0 {
let it = items[i]
if horizontal {
// y(y).x1(x).x0(xw)
x0z[i] = it.num0("x") + it.num0("width")
y0z[i] = it.num0("y")
curve.point(ctx, it.num0("x"), y0z[i])
} else {
// x(x).y1(y).y0(yh)
x0z[i] = it.num0("x")
y0z[i] = it.num0("y") + it.num0("height")
curve.point(ctx, x0z[i], it.num0("y"))
}
}
}
}
// ---- path strings for SVG ----
///|
/// `d` of a rect item (full precision), optionally offset.
pub fn rect_path(item : Item, x? : Double, y? : Double) -> String {
let p = PathBuf::new()
draw_rect(Buf(p), item, x0?=x, y0?=y)
p.to_string()
}
///|
/// `d` of a symbol item, centred on the origin (3 decimals, as d3-shape).
pub fn symbol_path(item : Item) -> String raise SceneError {
let p = PathBuf::new(digits=3)
draw_item_symbol(Buf(p), item)
p.to_string()
}
///|
pub fn line_path(items : Array[Item]) -> String raise SceneError {
let p = PathBuf::new(digits=3)
draw_line(Buf(p), items)
p.to_string()
}
///|
pub fn area_path(items : Array[Item]) -> String raise SceneError {
let p = PathBuf::new(digits=3)
draw_area(Buf(p), items)
p.to_string()
}