// Port of rough.js 4.6.6 src/generator.ts (RoughGenerator).
///|
/// Generates `Drawable`s: rough.js' `RoughGenerator` (`rough.generator()`).
pub struct Generator {
/// The options used when a shape gets no options of its own. As in
/// rough.js this is a single object, so its random number generator
/// carries on from one such shape to the next.
default_options : ResolvedOptions
}
///|
/// `rough.generator({ options })`. `source` replaces `Math.random()` (seed 0,
/// dot positions, hachure fallback); the default is
/// `RandomSource::default()`, a fixed-seed Park-Miller sequence.
pub fn Generator::new(
options? : Options,
source? : @random.RandomSource,
) -> Generator {
let source = match source {
Some(source) => source
None => @random.RandomSource::default()
}
let defaults = default_options(source)
let default_options = match options {
Some(options) => ResolvedOptions::merge(defaults, options)
None => defaults
}
{ default_options, }
}
///|
/// `RoughGenerator.newSeed()`: a random seed in `[0, 2^31)` from the
/// generator's random source.
pub fn Generator::new_seed(self : Generator) -> Int32 {
@random.new_seed(self.default_options.source)
}
///|
/// `_o(options)`: the defaults themselves when no options are given.
fn Generator::resolve(self : Generator, options : Options?) -> ResolvedOptions {
match options {
Some(options) => ResolvedOptions::merge(self.default_options, options)
None => self.default_options
}
}
///|
fn to_points(points : Array[(Double, Double)]) -> Array[Point] {
points.map(fn(p) { Point::new(p.0, p.1) })
}
///|
/// A line from (x1, y1) to (x2, y2).
pub fn Generator::line(
self : Generator,
x1 : Double,
y1 : Double,
x2 : Double,
y2 : Double,
options? : Options,
) -> Drawable {
let o = self.resolve(options)
{ shape: Line, sets: [line(x1, y1, x2, y2, o)], options: o, }
}
///|
/// A rectangle with top-left corner (x, y).
pub fn Generator::rectangle(
self : Generator,
x : Double,
y : Double,
width : Double,
height : Double,
options? : Options,
) -> Drawable {
let o = self.resolve(options)
let paths = []
let outline = rectangle(x, y, width, height, o)
if has_fill(o) {
let points = rectangle_points(x, y, width, height)
if o.fill_style == Solid {
paths.push(solid_fill_polygon([points], o))
} else {
paths.push(pattern_fill_polygons([points], o))
}
}
if o.stroke != "none" {
paths.push(outline)
}
{ shape: Rectangle, sets: paths, options: o, }
}
///|
/// An ellipse centered at (x, y).
pub fn Generator::ellipse(
self : Generator,
x : Double,
y : Double,
width : Double,
height : Double,
options? : Options,
) -> Drawable {
let o = self.resolve(options)
let paths = []
let ellipse_params = generate_ellipse_params(width, height, o)
let ellipse_response = ellipse_with_params(x, y, o, ellipse_params)
if has_fill(o) {
if o.fill_style == Solid {
// A second, independent pass over the ellipse (more random numbers).
let shape = ellipse_with_params(x, y, o, ellipse_params).opset
paths.push({ kind: FillPath, ops: shape.ops, })
} else {
paths.push(pattern_fill_polygons([ellipse_response.estimated_points], o))
}
}
if o.stroke != "none" {
paths.push(ellipse_response.opset)
}
{ shape: Ellipse, sets: paths, options: o, }
}
///|
/// A circle centered at (x, y).
pub fn Generator::circle(
self : Generator,
x : Double,
y : Double,
diameter : Double,
options? : Options,
) -> Drawable {
let ret = self.ellipse(x, y, diameter, diameter, options?)
{ ..ret, shape: Circle, }
}
///|
/// An open polyline through `points`.
pub fn Generator::linear_path(
self : Generator,
points : Array[(Double, Double)],
options? : Options,
) -> Drawable {
let o = self.resolve(options)
{
shape: LinearPath,
sets: [linear_path(to_points(points), false, o)],
options: o,
}
}
///|
/// An arc of the ellipse centered at (x, y), from angle `start` to `stop`
/// (radians). `closed` draws the two radii and allows a fill.
pub fn Generator::arc(
self : Generator,
x : Double,
y : Double,
width : Double,
height : Double,
start : Double,
stop : Double,
closed~ : Bool = false,
options? : Options,
) -> Drawable {
let o = self.resolve(options)
let paths = []
let outline = arc(x, y, width, height, start, stop, closed, true, o)
if closed && has_fill(o) {
if o.fill_style == Solid {
let fill_options = { ..o, disable_multi_stroke: true, }
let shape = arc(
x, y, width, height, start, stop, true, false, fill_options,
)
paths.push({ kind: FillPath, ops: shape.ops, })
} else {
paths.push(pattern_fill_arc(x, y, width, height, start, stop, o))
}
}
if o.stroke != "none" {
paths.push(outline)
}
{ shape: Arc, sets: paths, options: o, }
}
///|
/// A smooth curve through `points`.
pub fn Generator::curve(
self : Generator,
points : Array[(Double, Double)],
options? : Options,
) -> Drawable {
let points_list = if points.length() > 0 { [to_points(points)] } else { [] }
self.curve_points(points_list, options)
}
///|
/// Several curves drawn as one shape (rough.js' `curve(Point[][])`).
pub fn Generator::curves(
self : Generator,
points_list : Array[Array[(Double, Double)]],
options? : Options,
) -> Drawable {
self.curve_points(points_list.map(to_points), options)
}
///|
fn Generator::curve_points(
self : Generator,
points_list : Array[Array[Point]],
options : Options?,
) -> Drawable {
let o = self.resolve(options)
let paths = []
let outline = curve(points_list, o)
if has_fill(o) && o.fill != Some("none") {
if o.fill_style == Solid {
let fill_shape = curve(points_list, {
..o,
disable_multi_stroke: true,
roughness: if truthy(o.roughness) {
o.roughness + o.fill_shape_roughness_gain
} else {
0.0
},
})
paths.push({ kind: FillPath, ops: merged_shape(fill_shape.ops), })
} else {
let poly_points = []
let distance = (1.0 + o.roughness) / 2.0
for points in points_list {
if points.length() < 3 {
poly_points.append(points)
} else if points.length() == 3 {
let bezier = @curve.curve_to_bezier([
points[0],
points[0],
points[1],
points[2],
]) catch {
_ => panic()
}
poly_points.append(
@curve.points_on_bezier_curves(bezier, tolerance=10.0, distance~),
)
} else {
let bezier = @curve.curve_to_bezier(points) catch { _ => panic() }
poly_points.append(
@curve.points_on_bezier_curves(bezier, tolerance=10.0, distance~),
)
}
}
if poly_points.length() > 0 {
paths.push(pattern_fill_polygons([poly_points], o))
}
}
}
if o.stroke != "none" {
paths.push(outline)
}
{ shape: Curve, sets: paths, options: o, }
}
///|
/// A closed polygon through `points`.
pub fn Generator::polygon(
self : Generator,
points : Array[(Double, Double)],
options? : Options,
) -> Drawable {
let o = self.resolve(options)
let points = to_points(points)
let paths = []
let outline = linear_path(points, true, o)
if has_fill(o) {
if o.fill_style == Solid {
paths.push(solid_fill_polygon([points], o))
} else {
paths.push(pattern_fill_polygons([points], o))
}
}
if o.stroke != "none" {
paths.push(outline)
}
{ shape: Polygon, sets: paths, options: o, }
}
///|
/// The shape described by SVG path data `d`. Raises on malformed data,
/// where rough.js throws (or never returns).
pub fn Generator::path(
self : Generator,
d : StringView,
options? : Options,
) -> Drawable raise @path_data.PathError {
let o = self.resolve(options)
let paths = []
if d.length() == 0 {
return { shape: Path, sets: paths, options: o, }
}
let d = preprocess_path(d)
let has_fill = has_fill(o) &&
o.fill != Some("transparent") &&
o.fill != Some("none")
let has_stroke = o.stroke != "none"
let simplified = match o.simplification {
Some(s) => truthy(s) && s < 1.0
None => false
}
let distance = if simplified {
4.0 - 4.0 * js_or(o.simplification.unwrap_or(0.0), 1.0)
} else {
(1.0 + o.roughness) / 2.0
}
let sets = @curve.points_on_path(d, tolerance=1.0, distance~)
let shape = svg_path(d, o)
if has_fill {
if o.fill_style == Solid {
if sets.length() == 1 {
let fill_shape = svg_path(d, {
..o,
disable_multi_stroke: true,
roughness: if truthy(o.roughness) {
o.roughness + o.fill_shape_roughness_gain
} else {
0.0
},
})
paths.push({ kind: FillPath, ops: merged_shape(fill_shape.ops), })
} else {
paths.push(solid_fill_polygon(sets, o))
}
} else {
paths.push(pattern_fill_polygons(sets, o))
}
}
if has_stroke {
if simplified {
for set in sets {
paths.push(linear_path(set, false, o))
}
} else {
paths.push(shape)
}
}
{ shape: Path, sets: paths, options: o, }
}
///|
/// The string clean-up `RoughGenerator.path` applies first:
/// `d.replace(/\n/g, ' ').replace(/(-\s)/g, '-')`, then the third replace
/// of the published bundle, `.replace("/(ss)/g", " ")` (a plain-string
/// replace of its first occurrence).
fn preprocess_path(d : StringView) -> String {
let buf = StringBuilder()
let n = d.length()
let mut i = 0
while i < n {
let c = d.unsafe_get(i)
if c == '-' && i + 1 < n && is_js_whitespace(d.unsafe_get(i + 1)) {
// `-` followed by any \s (a \n has become a space by now) is `-`.
buf.write_char('-')
i += 2
} else {
buf.write_char(if c == '\n' { ' ' } else { c.to_int().unsafe_to_char() })
i += 1
}
}
buf.to_string().replace(old="/(ss)/g", new=" ")
}
///|
/// JavaScript's `\s` (WhiteSpace and LineTerminator), one UTF-16 unit.
fn is_js_whitespace(c : UInt16) -> Bool {
match c.to_int() {
0x09
| 0x0a
| 0x0b
| 0x0c
| 0x0d
| 0x20
| 0xa0
| 0x1680
| 0x2028
| 0x2029
| 0x202f
| 0x205f
| 0x3000
| 0xfeff => true
c => c >= 0x2000 && c <= 0x200a
}
}
///|
/// `_mergedShape`: drops every `move` but the first op.
fn merged_shape(input : Array[Op]) -> Array[Op] {
let out = []
for i, d in input {
if i == 0 || !(d is Move(_, _)) {
out.push(d)
}
}
out
}
///|
/// `opsToPath(drawing, fixedDecimals)`: SVG path data for an op set. With
/// `fixed_decimals`, numbers are rounded like `+n.toFixed(fixed_decimals)`.
pub fn Generator::ops_to_path(
self : Generator,
drawing : OpSet,
fixed_decimals? : Int32,
) -> String {
ignore(self)
ops_to_path(drawing, fixed_decimals?)
}
///|
/// `opsToPath` (see `Generator::ops_to_path`).
pub fn ops_to_path(drawing : OpSet, fixed_decimals? : Int32) -> String {
let fmt = match fixed_decimals {
Some(digits) if digits >= 0 =>
fn(x : Double) {
@jsmath.number_to_string(
@jsmath.parse_float(@jsmath.to_fixed(x, digits)),
)
}
_ => @jsmath.number_to_string
}
let path = StringBuilder()
for item in drawing.ops {
match item {
Move(x, y) => path.write_string("M\{fmt(x)} \{fmt(y)} ")
BCurveTo(x1, y1, x2, y2, x, y) =>
path.write_string(
"C\{fmt(x1)} \{fmt(y1)}, \{fmt(x2)} \{fmt(y2)}, \{fmt(x)} \{fmt(y)} ",
)
LineTo(x, y) => path.write_string("L\{fmt(x)} \{fmt(y)} ")
}
}
path.to_string().trim().to_owned()
}
///|
/// `toPaths(drawable)`: one `PathInfo` per op set.
pub fn Generator::to_paths(
self : Generator,
drawable : Drawable,
) -> Array[PathInfo] {
ignore(self)
let o = drawable.options
drawable.sets.map(fn(drawing) {
match drawing.kind {
Path =>
{
d: ops_to_path(drawing),
stroke: o.stroke,
stroke_width: o.stroke_width,
fill: Some("none"),
}
FillPath =>
{
d: ops_to_path(drawing),
stroke: "none",
stroke_width: 0.0,
fill: Some(o.fill.unwrap_or("none")),
}
FillSketch => fill_sketch(drawing, o)
}
})
}
///|
fn fill_sketch(drawing : OpSet, o : ResolvedOptions) -> PathInfo {
let mut fweight = o.fill_weight
if fweight < 0.0 {
fweight = o.stroke_width / 2.0
}
{
d: ops_to_path(drawing),
stroke: o.fill.unwrap_or("none"),
stroke_width: fweight,
fill: Some("none"),
}
}