///|
/// Render one trace frame as a deterministic, standalone SVG document.
#deprecated("Legacy static renderer; use render_trace_html or Schema v1 JSON. This API will be removed in v1.0.", skip_current_package=true)
pub fn render_trace_svg(
trace : AlgorithmTrace,
step? : Int = -1,
width? : Int = 900,
) -> String raise {
let scene = trace_scene_at(trace, step)
let height = scene_height(scene)
let out = StringBuilder()
out.write_string(
"\n")
out.to_string()
}
///|
/// Render every recorded step. An empty trace yields its initial scene.
#deprecated("Legacy static renderer; use render_trace_html or Schema v1 JSON. This API will be removed in v1.0.", skip_current_package=true)
pub fn render_trace_svg_frames(
trace : AlgorithmTrace,
width? : Int = 900,
) -> Array[String] raise {
if trace.steps.is_empty() {
[render_trace_svg(trace, step=-1, width~)]
} else {
trace.steps.map(fn(step) raise {
render_trace_svg(trace, step=step.index, width~)
})
}
}
///|
fn trace_scene_at(trace : AlgorithmTrace, step : Int) -> Scene raise {
if trace.steps.is_empty() || step < 0 {
trace.initial_scene
} else if step >= trace.steps.length() {
fail("Trace step is outside the recorded range")
} else {
trace.steps[step].scene
}
}
///|
fn trace_annotation_at(trace : AlgorithmTrace, step : Int) -> Annotation? {
if step < 0 || step >= trace.steps.length() {
None
} else {
trace.steps[step].annotation
}
}
///|
fn scene_height(scene : Scene) -> Int {
let mut height = 150
for object in scene.objects {
height += match object {
Sequence(_) => 120
Sets(value) => 86 + value.groups.length() * 46
Graph(value) => 130 + (value.nodes.length() + 3) / 4 * 90
Grid(value) => 56 + value.height * 44
}
}
height
}
///|
fn render_sequence_svg(
out : StringBuilder,
value : SequenceState,
scene : Scene,
y : Int,
) -> Int {
svg_text(out, 28, y + 20, value.label, 16, "#e2e8f0")
for i, item in value.items {
let x = 28 + i * 72
svg_box(out, x, y + 34, 58, 48, highlight_color(scene, value.id, item.id))
svg_text(out, x + 29, y + 64, item.value, 15, "#0f172a", anchor="middle")
svg_text(out, x + 29, y + 100, item.id, 10, "#94a3b8", anchor="middle")
}
y + 120
}
///|
fn render_sets_svg(
out : StringBuilder,
value : SetState,
scene : Scene,
y : Int,
) -> Int {
svg_text(out, 28, y + 20, value.label, 16, "#e2e8f0")
let mut row = y + 36
for group in value.groups {
let members = group.members.join(", ")
svg_box(out, 28, row, 820, 34, highlight_color(scene, value.id, group.id))
svg_text(out, 42, row + 22, "\{group.label}: \{members}", 13, "#0f172a")
row += 46
}
row + 24
}
///|
fn render_graph_svg(
out : StringBuilder,
value : GraphState,
scene : Scene,
y : Int,
) -> Int {
svg_text(out, 28, y + 20, value.label, 16, "#e2e8f0")
let points = layout_graph(value)
out.write_string(
"\n",
)
for edge in value.edges {
let from = graph_point(points, edge.from)
let to = graph_point(points, edge.to)
let marker = if edge.directed {
" marker-end=\"url(#frontierlab-arrow)\""
} else {
""
}
if edge.from == edge.to {
out.write_string(
"\n",
)
} else {
out.write_string(
"\n",
)
}
if edge.label != "" {
svg_text(
out,
(from.x + to.x) / 2,
y + (from.y + to.y) / 2 - 10,
edge.label,
11,
"#cbd5e1",
anchor="middle",
)
}
}
for node in value.nodes {
let point = graph_point(points, node.id)
out.write_string(
"\n",
)
svg_text(
out,
point.x,
y + point.y + 6,
node.label,
13,
"#0f172a",
anchor="middle",
)
}
y + 110 + (value.nodes.length() + 3) / 4 * 90
}
///|
fn render_grid_svg(
out : StringBuilder,
value : GridState,
scene : Scene,
y : Int,
) -> Int {
svg_text(out, 28, y + 20, value.label, 16, "#e2e8f0")
for cell in value.cells {
let x = 28 + cell.x * 44
let cy = y + 34 + cell.y * 44
let fill = if cell.blocked {
"#1e293b"
} else {
highlight_color(scene, value.id, cell.id)
}
svg_box(out, x, cy, 40, 40, fill)
if cell.label != "" {
svg_text(
out,
x + 20,
cy + 25,
cell.label,
11,
if cell.blocked {
"#f8fafc"
} else {
"#0f172a"
},
anchor="middle",
)
}
}
y + 56 + value.height * 44
}
///|
priv struct GraphLayoutPoint {
id : String
x : Int
y : Int
}
///|
fn graph_point(
points : Array[GraphLayoutPoint],
id : String,
) -> GraphLayoutPoint {
points[points.search_by(fn(point) { point.id == id }).unwrap()]
}
///|
fn layout_graph(graph : GraphState) -> Array[GraphLayoutPoint] {
let count = graph.nodes.length()
let indegree = Array::make(count, 0)
let level = Array::make(count, 0)
for edge in graph.edges {
if edge.from != edge.to {
let to = graph.nodes.search_by(fn(node) { node.id == edge.to }).unwrap()
indegree[to] += 1
}
}
let queue : Array[Int] = []
for i in 0.. String {
let target = TargetRef::entity(object_id, entity_id)
match scene.highlights.search_by(fn(item) { item.target == target }) {
None => "#f8fafc"
Some(index) =>
match scene.highlights[index].role {
Current => "#f59e0b"
Candidate => "#38bdf8"
Compared => "#c084fc"
Changed => "#fb7185"
Visited => "#93c5fd"
Frontier => "#22d3ee"
Result => "#4ade80"
Error => "#ef4444"
}
}
}
///|
fn svg_box(
out : StringBuilder,
x : Int,
y : Int,
width : Int,
height : Int,
fill : String,
) -> Unit {
out.write_string(
"\n",
)
}
///|
fn svg_text(
out : StringBuilder,
x : Int,
y : Int,
text : String,
size : Int,
color : String,
anchor? : String = "start",
) -> Unit {
out.write_string(
"\{escape_markup(text)}\n",
)
}
///|
fn escape_markup(text : String) -> String {
let out = StringBuilder()
for char in text {
match char {
'&' => out.write_string("&")
'<' => out.write_string("<")
'>' => out.write_string(">")
'"' => out.write_string(""")
'\'' => out.write_string("'")
_ => out.write_char(char)
}
}
out.to_string()
}