///|
/// World-space bounds for a shared drawing scale. Must contain all geometry.
pub(all) struct SvgBounds {
min_x : Double
min_y : Double
max_x : Double
max_y : Double
} derive(Debug)
///|
pub extend SvgBounds with @debug.Debug::{to_repr}
///|
pub(all) struct SvgOptions {
width : Int32
height : Int32
padding : Double
title : String
show_points : Bool
bounds : SvgBounds?
} derive(Debug)
///|
pub extend SvgOptions with @debug.Debug::{to_repr}
///|
pub fn SvgOptions::default() -> SvgOptions {
{
width: 720,
height: 440,
padding: 24.0,
title: "CAD sketch",
show_points: true,
bounds: None,
}
}
///|
fn svg_escape(text : String) -> String {
let out = StringBuilder()
for c in text {
match c {
'&' => out.write_string("&")
'<' => out.write_string("<")
'>' => out.write_string(">")
'"' => out.write_string(""")
'\'' => out.write_string("'")
'\n' | '\r' | '\t' => out.write_string(" ")
_ => {
let n = c.to_int()
if (n >= 32 && n <= 0xD7FF) ||
(n >= 0xE000 && n <= 0xFFFD) ||
n >= 0x10000 {
out.write_char(c)
} else {
out.write_string("�")
}
}
}
}
out.to_string()
}
///|
/// Export current stored geometry. Does not solve or apply candidate solutions.
/// Checks geometry and enabled constraint definitions using normal validation.
/// Lines are finite segments; circles keep their shape under uniform scaling.
pub fn Sketch::to_svg(
self : Sketch,
options~ : SvgOptions = SvgOptions::default(),
) -> Result[String, SketchError] {
if options.width < 64 ||
options.width > 4096 ||
options.height < 96 ||
options.height > 4096 ||
!finite(options.padding) ||
options.padding < 4.0 ||
options.width.to_double() - 2.0 * options.padding <= 0.0 ||
options.height.to_double() - 2.0 * options.padding - 32.0 <= 0.0 {
return Err(
InvalidOptions(
"SVG dimensions must be 64..4096 by 96..4096 with room for padding and title",
),
)
}
match self.validate() {
Err(error) => return Err(error)
Ok(_) => ()
}
let first = self.points.get(0).unwrap_or({ x: 0.0, y: 0.0, })
let mut min_x = first.x
let mut max_x = first.x
let mut min_y = first.y
let mut max_y = first.y
let mut diameter = 0.0
for p in self.points {
min_x = if p.x < min_x { p.x } else { min_x }
max_x = dmax(max_x, p.x)
min_y = if p.y < min_y { p.y } else { min_y }
max_y = dmax(max_y, p.y)
}
for c in self.circles {
let p = self.points[c.center.index]
min_x = if p.x - c.radius < min_x { p.x - c.radius } else { min_x }
max_x = dmax(max_x, p.x + c.radius)
min_y = if p.y - c.radius < min_y { p.y - c.radius } else { min_y }
max_y = dmax(max_y, p.y + c.radius)
diameter = dmax(diameter, 2.0 * c.radius)
}
match options.bounds {
None => ()
Some(b) => {
if !finite(b.min_x) ||
!finite(b.min_y) ||
!finite(b.max_x) ||
!finite(b.max_y) ||
b.min_x >= b.max_x ||
b.min_y >= b.max_y ||
!finite(b.max_x - b.min_x) ||
!finite(b.max_y - b.min_y) ||
(
self.points.length() > 0 &&
(
b.min_x > min_x ||
b.max_x < max_x ||
b.min_y > min_y ||
b.max_y < max_y
)
) {
return Err(
InvalidOptions(
"SVG bounds must be finite, ordered and contain all stored geometry",
),
)
}
min_x = b.min_x
min_y = b.min_y
max_x = b.max_x
max_y = b.max_y
for c in self.circles {
let p = self.points[c.center.index]
if c.radius > p.x - min_x ||
c.radius > max_x - p.x ||
c.radius > p.y - min_y ||
c.radius > max_y - p.y {
return Err(InvalidOptions("SVG bounds must contain full circles"))
}
}
}
}
// Normalize before converting to pixels, including subnormal-sized geometry.
// Radius contributes separately when center +/- radius rounds to the center.
let world_width = dmax(max_x - min_x, diameter)
let world_height = dmax(max_y - min_y, diameter)
let span = dmax(world_width, world_height)
let span = if span > 0.0 { span } else { 1.0 }
let avail_width = options.width.to_double() - 2.0 * options.padding
let avail_height = options.height.to_double() - 2.0 * options.padding - 32.0
let pixels = if world_width >= world_height {
let other = if world_height > 0.0 {
avail_height / (world_height / span)
} else {
avail_width
}
if avail_width < other {
avail_width
} else {
other
}
} else {
let other = if world_width > 0.0 {
avail_width / (world_width / span)
} else {
avail_height
}
if avail_height < other {
avail_height
} else {
other
}
}
let center_x = min_x / 2.0 + max_x / 2.0
let center_y = min_y / 2.0 + max_y / 2.0
let screen_x = (x : Double) => {
options.width.to_double() / 2.0 + (x - center_x) / span * pixels
}
let screen_y = (y : Double) => {
32.0 +
(options.height.to_double() - 32.0) / 2.0 -
(y - center_y) / span * pixels
}
let title = svg_escape(options.title)
let out = StringBuilder()
out.write_string(
"\n")
Ok(out.to_string())
}