///|
priv struct InputObject {
kind : String
thread : Double
path : Array[Array[Double]]?
left : Array[Array[Double]]?
right : Array[Array[Double]]?
rings : Array[Array[Array[Double]]]?
spacing : Double?
} derive(FromJson)
///|
priv struct Input {
width : Double
height : Double
max_step : Double
objects : Array[InputObject]
} derive(FromJson)
///|
fn int_valid(x : Double, low : Int32, high : Int32) -> Bool {
x >= low.to_double() && x <= high.to_double() && x.to_int().to_double() == x
}
///|
/// Check lexical integers before Double decoding can round a fractional token.
fn integer_tokens(value : Json) -> Bool {
match value {
Number(_, repr~) => {
let s = match repr {
None => return false
Some(s) => s
}
let mut first = true
let mut digits = 0
for c in s.iter() {
if first && c == '-' {
first = false
continue
}
if c < '0' || c > '9' {
return false
}
first = false
digits = digits + 1
}
digits > 0
}
Array(items) => items.all(integer_tokens)
Object(fields) => {
for _, v in fields {
if !integer_tokens(v) {
return false
}
}
true
}
_ => true
}
}
///|
fn points(raw : Array[Array[Double]]) -> Result[Array[Point], String] {
if raw.length() > 10000 {
return Err("too many points")
}
let out : Array[Point] = []
for p in raw {
if p.length() != 2 ||
!int_valid(p[0], -10000, 10000) ||
!int_valid(p[1], -10000, 10000) {
return Err("points require two integer coordinates in -10000..10000")
}
out.push({ x: p[0].to_int(), y: p[1].to_int(), })
}
Ok(out)
}
///|
pub fn parse_job(text : String) -> Result[Plan, String] {
if text.length() > 4000000 {
return Err("job text limit: 4000000")
}
let raw = @json.parse(
text,
reject_duplicate_keys=true,
max_nesting_depth=16,
preserve_number_repr=true,
) catch {
_ => return Err("invalid JSON")
}
if !integer_tokens(raw) {
return Err(
"job numbers must use integer literals (no decimals or exponents)",
)
}
let input : Input = @json.from_json(raw) catch {
_ => return Err("invalid job schema")
}
match raw {
Object(fields) => {
for key, _ in fields {
if !["width", "height", "max_step", "objects"].contains(key) {
return Err("unknown job field: " + key)
}
}
if fields.get("objects") is Some(Array(items)) {
for item in items {
if item is Object(obj_fields) {
for key, _ in obj_fields {
if ![
"kind", "thread", "path", "left", "right", "rings", "spacing",
].contains(key) {
return Err("unknown object field: " + key)
}
}
}
}
}
}
_ => return Err("job must be an object")
}
if !int_valid(input.width, 100, 10000) ||
!int_valid(input.height, 100, 10000) ||
!int_valid(input.max_step, 3, 121) {
return Err("hoop and step must be bounded integers")
}
if input.objects.is_empty() || input.objects.length() > 256 {
return Err("object count must be 1..256")
}
let objects : Array[Object] = []
for obj in input.objects {
if !int_valid(obj.thread, 0, 63) {
return Err("invalid thread id")
}
let shape = match obj.kind {
"running" => {
if obj.left is Some(_) ||
obj.right is Some(_) ||
obj.rings is Some(_) ||
obj.spacing is Some(_) {
return Err("irrelevant running parameters")
}
let raw = match obj.path {
None => return Err("running requires path")
Some(v) => v
}
Running(
match points(raw) {
Err(e) => return Err(e)
Ok(v) => v
},
)
}
"satin" => {
if obj.path is Some(_) || obj.rings is Some(_) {
return Err("irrelevant satin parameters")
}
let l = match obj.left {
None => return Err("satin requires left rail")
Some(v) => v
}
let r = match obj.right {
None => return Err("satin requires right rail")
Some(v) => v
}
let s = match obj.spacing {
None => return Err("satin requires spacing")
Some(v) => v
}
if !int_valid(s, 3, 1000) {
return Err("invalid satin spacing")
}
Satin(
match points(l) {
Err(e) => return Err(e)
Ok(v) => v
},
match points(r) {
Err(e) => return Err(e)
Ok(v) => v
},
s.to_int(),
)
}
"fill" => {
if obj.path is Some(_) || obj.left is Some(_) || obj.right is Some(_) {
return Err("irrelevant fill parameters")
}
let raw = match obj.rings {
None => return Err("fill requires rings")
Some(v) => v
}
if raw.length() > 16 {
return Err("too many rings")
}
let s = match obj.spacing {
None => return Err("fill requires spacing")
Some(v) => v
}
if !int_valid(s, 2, 1000) {
return Err("invalid fill spacing")
}
let rings : Array[Array[Point]] = []
for r in raw {
rings.push(
match points(r) {
Err(e) => return Err(e)
Ok(v) => v
},
)
}
Fill(rings, s.to_int())
}
_ => return Err("unknown stitch kind")
}
objects.push({ shape, thread: obj.thread.to_int(), })
}
compile(
objects,
input.width.to_int(),
input.height.to_int(),
input.max_step.to_int(),
)
}
///|
/// Pure compilation: host writes the returned bytes, SVG and report.
pub fn compile_json(text : String) -> String {
let p = match parse_job(text) {
Err(e) => return ({ "error": Json(e) } : Json).stringify()
Ok(p) => p
}
let bytes = p.dst()
let out : Json = {
"dst_bytes": Json(Array::makei(bytes.length(), fn(i) { bytes[i].to_int() })),
"svg": Json(p.svg()),
"report": p.report(),
}
out.stringify()
}