// arbor-render — SVG emission from a laid-out diagram. // // Entry point: // fn arbor_render_svg(graph: Map, layout: Map, forbidden: [String]) -> String // // The graph is the lowered (diagram-form) shape produced by arbor-core / // arbor-diagram (`title`, `direction`, `nodes`, `edges`, `groups`). The // layout is whatever arbor-layout returned: `node_pos_`, `node_size_`, // `group_bounds_`, `node_ids`, `group_ids`, `canvas`. // // `forbidden` is a list of "from->to" key strings — same format as // arbor-core's collect_forbidden(). The Rust crate threaded a HashSet // through; El threads a list and we linear-scan. // // SVG is text emission — straightforward El. Every float coordinate is // passed through format_float(_, 1) for stable output. // // ── PNG render is intentionally out of scope ──────────────────────────────── // The Rust crate rasterises via resvg → tiny_skia → png. The El runtime // today exposes no equivalent: there is no resvg, no usvg, no font rasterer, // no PNG encoder, no path-fill code. fs_write writes text only — there is // no binary write primitive. arbor_render_png() returns an error map in El // until the runtime grows a rasterer (see "runtime gaps" in the report). // ── Colour palette (matches the Rust constants exactly) ──────────────────── fn col_node_fill() -> String { "#ffffff" } fn col_node_stroke() -> String { "#334155" } fn col_primary_fill() -> String { "#0052A0" } fn col_primary_text() -> String { "#ffffff" } fn col_node_text() -> String { "#0D0D14" } fn col_edge() -> String { "#64748B" } fn col_edge_forbidden() -> String { "#DC2626" } fn col_group_fill() -> String { "rgba(0,0,0,0.03)" } fn col_group_stroke() -> String { "#CBD5E1" } fn col_group_text() -> String { "#64748B" } fn col_edge_label() -> String { "#64748B" } // ── XML escape ───────────────────────────────────────────────────────────── fn esc(s: String) -> String { let r1: String = str_replace(s, "&", "&") let r2: String = str_replace(r1, "<", "<") let r3: String = str_replace(r2, ">", ">") let r4: String = str_replace(r3, "\"", """) r4 } // Float to "%.1f" — the Rust pt() helper. fn pt(v: el_val_t) -> String { format_float(v, 1) } // Float arithmetic helpers — float_to_int / int_to_float trip through Int, // which is exact for the integer-valued floats used by the layout pass. fn fadd(a: el_val_t, b: el_val_t) -> el_val_t { let ai: Int = float_to_int(a) let bi: Int = float_to_int(b) int_to_float(ai + bi) } fn fsub(a: el_val_t, b: el_val_t) -> el_val_t { let ai: Int = float_to_int(a) let bi: Int = float_to_int(b) int_to_float(ai - bi) } fn fdiv2(a: el_val_t) -> el_val_t { let ai: Int = float_to_int(a) int_to_float(ai / 2) } fn fmid(a: el_val_t, b: el_val_t) -> el_val_t { fdiv2(fadd(a, b)) } // ── forbidden-edge linear lookup ─────────────────────────────────────────── fn forbidden_key(from: String, to: String) -> String { from + "->" + to } fn forbidden_contains(set: [String], src: String, dst: String) -> Bool { let key: String = forbidden_key(src, dst) let n: Int = el_list_len(set) let i = 0 while i < n { let s: String = get(set, i) if str_eq(s, key) { return true } let i = i + 1 } false } // ── Arrow marker defs ────────────────────────────────────────────────────── fn arrow_defs() -> String { let s = "\n \n" let s = s + " \n" let s = s + " \n" let s = s + " \n" let s = s + " \n" let s = s + " \n" let s = s + " \n" let s = s + " \n" let s = s + " " s } // ── Node rendering ───────────────────────────────────────────────────────── fn render_node(buf: String, node: Map, layout: Map) -> String { let nid: String = node["id"] let pos: Map = el_map_get(layout, "node_pos_" + nid) let sz: Map = el_map_get(layout, "node_size_" + nid) let cx: el_val_t = pos["x"] let cy: el_val_t = pos["y"] let w: el_val_t = sz["w"] let h: el_val_t = sz["h"] let x: el_val_t = fsub(cx, fdiv2(w)) let y: el_val_t = fsub(cy, fdiv2(h)) let fill_in: String = node["style_fill"] let stroke_in: String = node["style_stroke"] let color_in: String = node["style_color"] let fill = col_node_fill() if str_len(fill_in) > 0 { let fill = fill_in } let stroke = col_node_stroke() if str_len(stroke_in) > 0 { let stroke = stroke_in } let text_col = col_node_text() if str_len(color_in) > 0 { let text_col = color_in } let shape: String = node["shape"] let buf = buf if str_eq(shape, "rectangle") { let buf = buf + " \n" } if str_eq(shape, "rounded_rect") { let buf = buf + " \n" } if str_eq(shape, "stadium") { let buf = buf + " \n" } if str_eq(shape, "cylinder") { // body: rect from y+ry to bottom; ry ≈ h/6 (Rust uses h*0.18, we use h/6 // to stay in integer arithmetic — visually indistinguishable on the // canvas sizes the layout produces). let hi: Int = float_to_int(h) let ry: el_val_t = int_to_float(hi / 6) let body_y: el_val_t = fadd(y, ry) let body_h: el_val_t = fsub(h, ry) let buf = buf + " \n" // top ellipse let buf = buf + " \n" // bottom ellipse let bot_y: el_val_t = fadd(y, h) let buf = buf + " \n" } if str_eq(shape, "diamond") { let hw: el_val_t = fdiv2(w) let hh: el_val_t = fdiv2(h) let buf = buf + " \n" } // Label. let label: String = node["label"] let buf = buf + " " let buf = buf + esc(label) + "\n" // Sublabel — Rust's DiagramNode stores Option; El uses "" sentinel. let sub: String = node["sublabel"] if str_len(sub) > 0 { let sub_y: el_val_t = fadd(cy, int_to_float(14)) let buf = buf + " " let buf = buf + esc(sub) + "\n" } buf } // ── Edge rendering ───────────────────────────────────────────────────────── // // We emit a straight line from one node centre to the other and let the // browser draw it; the Rust crate renders cubic bezier paths but the runtime // has no robust math layer, and the rectangles are large enough that // straight edges read clearly. (See "runtime gaps".) fn render_edge(buf: String, edge: Map, layout: Map, forbidden: [String]) -> String { let from_id: String = edge["from"] let to_id: String = edge["to"] let from_pos: Map = el_map_get(layout, "node_pos_" + from_id) let to_pos: Map = el_map_get(layout, "node_pos_" + to_id) let fx: el_val_t = from_pos["x"] let fy: el_val_t = from_pos["y"] let tx: el_val_t = to_pos["x"] let ty: el_val_t = to_pos["y"] let is_forbidden: Bool = forbidden_contains(forbidden, from_id, to_id) let stroke = col_edge() if is_forbidden { let stroke = col_edge_forbidden() } let line: String = edge["line"] let arrow: String = edge["arrow"] let dash_attr = "" if str_eq(line, "dashed") { let dash_attr = " stroke-dasharray=\"5,3\"" } if str_eq(line, "dotted") { let dash_attr = " stroke-dasharray=\"2,2\"" } let marker_start = "" if str_eq(arrow, "both") { let marker_start = " marker-start=\"url(#ah-bi)\"" } if str_eq(arrow, "backward") { let marker_start = " marker-start=\"url(#ah-bi)\"" } let marker_end = " marker-end=\"url(#ah)\"" if is_forbidden { let marker_end = " marker-end=\"url(#ah-red)\"" } if str_eq(arrow, "none") { let marker_end = "" } if str_eq(arrow, "backward") { let marker_end = "" } let buf = buf + " \n" // Forbidden marker — circle-X at midpoint. if is_forbidden { let mx: el_val_t = fmid(fx, tx) let my: el_val_t = fmid(fy, ty) let r: el_val_t = int_to_float(7) let buf = buf + " \n" let off: el_val_t = int_to_float(4) let buf = buf + " \n" let buf = buf + " \n" } // Edge label let label: String = edge["label"] if str_len(label) > 0 { let mx: el_val_t = fmid(fx, tx) let my: el_val_t = fmid(fy, ty) let lw: el_val_t = int_to_float(str_len(label) * 7 + 8) let lh: el_val_t = int_to_float(16) let buf = buf + " \n" let buf = buf + " " + esc(label) + "\n" } buf } // ── Group rendering ──────────────────────────────────────────────────────── fn render_group(buf: String, group: Map, layout: Map) -> String { let gid: String = group["id"] let bounds: Map = el_map_get(layout, "group_bounds_" + gid) // Layout may not have bounds for empty groups — defensive. let bx_check: el_val_t = bounds["x"] if float_to_int(bx_check) == 0 { // Could be a real 0; cheaper to skip via presence check on group_ids. } let bx: el_val_t = bounds["x"] let by: el_val_t = bounds["y"] let bw: el_val_t = bounds["w"] let bh: el_val_t = bounds["h"] let buf = buf + " \n" // Group label in the top-left corner. let lx: el_val_t = fadd(bx, int_to_float(8)) let ly: el_val_t = fadd(by, int_to_float(14)) let label: String = group["label"] let buf = buf + " " + esc(label) + "\n" buf } // ── Public entry point ───────────────────────────────────────────────────── fn arbor_render_svg(graph: Map, layout: Map, forbidden: [String]) -> String { let canvas: Map = el_map_get(layout, "canvas") let cw: el_val_t = canvas["w"] let ch: el_val_t = canvas["h"] let buf = "\n" let buf = buf + " " let buf = buf + arrow_defs() let buf = buf + "\n \n" let buf = buf + " \n" // Groups first (behind everything). let buf = buf + " \n" let groups: [Map] = graph["groups"] let gn: Int = el_list_len(groups) let i = 0 while i < gn { let g: Map = get(groups, i) let gid: String = g["id"] // Only render groups the layout actually placed. let gids: [String] = el_map_get(layout, "group_ids") let placed = false let j = 0 while j < el_list_len(gids) { if str_eq(get(gids, j), gid) { let placed = true } let j = j + 1 } if placed { let buf = render_group(buf, g, layout) } let i = i + 1 } // Edges let buf = buf + " \n" let edges: [Map] = graph["edges"] let en: Int = el_list_len(edges) let i = 0 while i < en { let e: Map = get(edges, i) let buf = render_edge(buf, e, layout, forbidden) let i = i + 1 } // Nodes let buf = buf + " \n" let nodes: [Map] = graph["nodes"] let nn: Int = el_list_len(nodes) let i = 0 while i < nn { let n: Map = get(nodes, i) let buf = render_node(buf, n, layout) let i = i + 1 } // Title let title: String = graph["title"] if str_len(title) > 0 { let title_x: el_val_t = fdiv2(cw) let buf = buf + " " let buf = buf + esc(title) + "\n" } let buf = buf + "\n" buf } // PNG — not implemented; the runtime has no SVG rasterizer or PNG encoder. // Returns an error map that callers can inspect via map["error"]. fn arbor_render_png(graph: Map, layout: Map, forbidden: [String]) -> Map { { "error": "PNG rasterization not available in El runtime — install a runtime image library or use the Rust binary" } } // ── Smoke test ───────────────────────────────────────────────────────────── fn fail(label: String, msg: String) -> Int { println("FAIL " + label + ": " + msg) state_set("smoke_failures", "1") 0 } fn check_contains(label: String, haystack: String, needle: String) -> Int { if str_contains(haystack, needle) { println("ok " + label) return 1 } fail(label, "missing [" + needle + "]") } fn check_not_contains(label: String, haystack: String, needle: String) -> Int { if str_contains(haystack, needle) { return fail(label, "should not contain [" + needle + "]") } println("ok " + label) 1 } fn make_test_node(id: String, label: String, shape: String) -> Map { { "id": id, "label": label, "sublabel": "", "shape": shape, "style_fill": "", "style_stroke": "", "style_color": "" } } fn make_test_edge(src: String, dst: String, line: String, arrow: String, label: String) -> Map { { "from": src, "to": dst, "label": label, "line": line, "arrow": arrow } } fn make_test_pos(x: Int, y: Int) -> Map { { "x": int_to_float(x), "y": int_to_float(y) } } fn make_test_size(w: Int, h: Int) -> Map { { "w": int_to_float(w), "h": int_to_float(h) } } // Build a minimal layout map by hand. fn build_layout(node_ids: [String], group_ids: [String], cw: Int, ch: Int) -> Map { let r: Map = el_map_new(0) let r = el_map_set(r, "node_ids", node_ids) let r = el_map_set(r, "group_ids", group_ids) let r = el_map_set(r, "canvas", { "w": int_to_float(cw), "h": int_to_float(ch) }) r } let n_a: Map = make_test_node("a", "Node A", "rectangle") let n_b: Map = make_test_node("b", "Node B", "rectangle") let e_ab: Map = make_test_edge("a", "b", "solid", "forward", "") let nodes: [Map] = native_list_empty() let nodes = native_list_append(nodes, n_a) let nodes = native_list_append(nodes, n_b) let edges: [Map] = native_list_empty() let edges = native_list_append(edges, e_ab) let groups: [Map] = native_list_empty() let g: Map = { "title": "Test", "direction": "top-down", "nodes": nodes, "edges": edges, "groups": groups } let nid_list: [String] = native_list_empty() let nid_list = native_list_append(nid_list, "a") let nid_list = native_list_append(nid_list, "b") let gid_list: [String] = native_list_empty() let layout: Map = build_layout(nid_list, gid_list, 400, 300) let layout = el_map_set(layout, "node_pos_a", make_test_pos(100, 60)) let layout = el_map_set(layout, "node_pos_b", make_test_pos(100, 200)) let layout = el_map_set(layout, "node_size_a", make_test_size(120, 40)) let layout = el_map_set(layout, "node_size_b", make_test_size(120, 40)) let forbidden: [String] = native_list_empty() let svg: String = arbor_render_svg(g, layout, forbidden) check_contains("svg starts with ", svg, "") check_contains("svg contains node label", svg, "Node A") check_contains("svg contains title", svg, ">Test") check_contains("svg has rect for rectangle node", svg, " = make_test_node("x", "A & B ", "rectangle") let nodes2: [Map] = native_list_empty() let nodes2 = native_list_append(nodes2, n_esc) let g2: Map = { "title": "Test ", "direction": "top-down", "nodes": nodes2, "edges": native_list_empty(), "groups": native_list_empty() } let nid2: [String] = native_list_empty() let nid2 = native_list_append(nid2, "x") let layout2: Map<String, Any> = build_layout(nid2, native_list_empty(), 200, 100) let layout2 = el_map_set(layout2, "node_pos_x", make_test_pos(80, 40)) let layout2 = el_map_set(layout2, "node_size_x", make_test_size(120, 40)) let svg2: String = arbor_render_svg(g2, layout2, native_list_empty()) check_contains("escapes ampersand", svg2, "&") check_contains("escapes <", svg2, "<") check_not_contains("no raw <C>", svg2, "<C>") // Forbidden edge let e_fb: Map<String, Any> = make_test_edge("a", "b", "solid", "forward", "") let edges3: [Map<String, Any>] = native_list_empty() let edges3 = native_list_append(edges3, e_fb) let g3: Map<String, Any> = { "title": "F", "direction": "top-down", "nodes": nodes, "edges": edges3, "groups": native_list_empty() } let fb: [String] = native_list_empty() let fb = native_list_append(fb, forbidden_key("a", "b")) let svg3: String = arbor_render_svg(g3, layout, fb) check_contains("forbidden uses red marker", svg3, "ah-red") check_contains("forbidden colour present", svg3, col_edge_forbidden()) // Diamond shape → polygon let n_d: Map<String, Any> = make_test_node("d", "Decide", "diamond") let g4: Map<String, Any> = { "title": "", "direction": "top-down", "nodes": native_list_append(native_list_empty(), n_d), "edges": native_list_empty(), "groups": native_list_empty() } let nid4: [String] = native_list_append(native_list_empty(), "d") let layout4: Map<String, Any> = build_layout(nid4, native_list_empty(), 200, 100) let layout4 = el_map_set(layout4, "node_pos_d", make_test_pos(80, 50)) let layout4 = el_map_set(layout4, "node_size_d", make_test_size(120, 40)) let svg4: String = arbor_render_svg(g4, layout4, native_list_empty()) check_contains("diamond uses polygon", svg4, "<polygon") // Cylinder shape → ellipses let n_cy: Map<String, Any> = make_test_node("cy", "DB", "cylinder") let g5: Map<String, Any> = { "title": "", "direction": "top-down", "nodes": native_list_append(native_list_empty(), n_cy), "edges": native_list_empty(), "groups": native_list_empty() } let nid5: [String] = native_list_append(native_list_empty(), "cy") let layout5: Map<String, Any> = build_layout(nid5, native_list_empty(), 200, 100) let layout5 = el_map_set(layout5, "node_pos_cy", make_test_pos(80, 50)) let layout5 = el_map_set(layout5, "node_size_cy", make_test_size(120, 40)) let svg5: String = arbor_render_svg(g5, layout5, native_list_empty()) check_contains("cylinder uses ellipse", svg5, "<ellipse") // Dashed edge let e_dash: Map<String, Any> = make_test_edge("a", "b", "dashed", "forward", "") let g6: Map<String, Any> = { "title": "", "direction": "top-down", "nodes": nodes, "edges": native_list_append(native_list_empty(), e_dash), "groups": native_list_empty() } let svg6: String = arbor_render_svg(g6, layout, native_list_empty()) check_contains("dashed line dasharray", svg6, "stroke-dasharray=\"5,3\"") // PNG returns an error map let png: Map<String, Any> = arbor_render_png(g, layout, native_list_empty()) let err: String = png["error"] if str_len(err) > 0 { println("ok PNG returns error map") } else { println("FAIL PNG should have returned error") state_set("smoke_failures", "1") } println("") let f: String = state_get("smoke_failures") if str_eq(f, "1") { println("arbor-render: FAILED") exit_program(1) } else { println("arbor-render: ok") }