feat: port arbor, dharma, forge El source into monorepo
Brings the remaining foundation repos that were not included in the original monorepo consolidation: - arbor/vessels/ — 6 vessels (arbor-cli, arbor-core, arbor-diagram, arbor-layout, arbor-parse, arbor-render) with manifests + src/main.el - dharma/ — CGI Provenance Registry package (flat layout, 14 .el files across registry/, sandbox/, training/, validation/, tests/) - forge/ — consciousness channel tool (8 src .el files + new manifest.el) - elp/src/ — 36 test fixture files not carried over in original merge (dedup_*, realizer_*, semantics_*, morph_*, ext_*, one_extern_* helpers) el-ide, engram, elql are already complete in ide/, engram/, ql/.
This commit is contained in:
@@ -0,0 +1,575 @@
|
||||
// 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_<id>`, `node_size_<id>`,
|
||||
// `group_bounds_<id>`, `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 <marker id=\"ah\" markerWidth=\"10\" markerHeight=\"7\" refX=\"9\" refY=\"3.5\" orient=\"auto\">\n"
|
||||
let s = s + " <polygon points=\"0 0, 10 3.5, 0 7\" fill=\"" + col_edge() + "\"/>\n"
|
||||
let s = s + " </marker>\n"
|
||||
let s = s + " <marker id=\"ah-bi\" markerWidth=\"10\" markerHeight=\"7\" refX=\"1\" refY=\"3.5\" orient=\"auto-start-reverse\">\n"
|
||||
let s = s + " <polygon points=\"0 0, 10 3.5, 0 7\" fill=\"" + col_edge() + "\"/>\n"
|
||||
let s = s + " </marker>\n"
|
||||
let s = s + " <marker id=\"ah-red\" markerWidth=\"10\" markerHeight=\"7\" refX=\"9\" refY=\"3.5\" orient=\"auto\">\n"
|
||||
let s = s + " <polygon points=\"0 0, 10 3.5, 0 7\" fill=\"" + col_edge_forbidden() + "\"/>\n"
|
||||
let s = s + " </marker>"
|
||||
s
|
||||
}
|
||||
|
||||
// ── Node rendering ─────────────────────────────────────────────────────────
|
||||
|
||||
fn render_node(buf: String, node: Map<String, Any>, layout: Map<String, Any>) -> String {
|
||||
let nid: String = node["id"]
|
||||
let pos: Map<String, Any> = el_map_get(layout, "node_pos_" + nid)
|
||||
let sz: Map<String, Any> = 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 + " <rect x=\"" + pt(x) + "\" y=\"" + pt(y)
|
||||
let buf = buf + "\" width=\"" + pt(w) + "\" height=\"" + pt(h)
|
||||
let buf = buf + "\" rx=\"4\" fill=\"" + fill + "\" stroke=\"" + stroke
|
||||
let buf = buf + "\" stroke-width=\"1.5\"/>\n"
|
||||
}
|
||||
if str_eq(shape, "rounded_rect") {
|
||||
let buf = buf + " <rect x=\"" + pt(x) + "\" y=\"" + pt(y)
|
||||
let buf = buf + "\" width=\"" + pt(w) + "\" height=\"" + pt(h)
|
||||
let buf = buf + "\" rx=\"20\" fill=\"" + fill + "\" stroke=\"" + stroke
|
||||
let buf = buf + "\" stroke-width=\"1.5\"/>\n"
|
||||
}
|
||||
if str_eq(shape, "stadium") {
|
||||
let buf = buf + " <rect x=\"" + pt(x) + "\" y=\"" + pt(y)
|
||||
let buf = buf + "\" width=\"" + pt(w) + "\" height=\"" + pt(h)
|
||||
let buf = buf + "\" rx=\"" + pt(fdiv2(h)) + "\" fill=\"" + fill
|
||||
let buf = buf + "\" stroke=\"" + stroke + "\" stroke-width=\"1.5\"/>\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 + " <rect x=\"" + pt(x) + "\" y=\"" + pt(body_y)
|
||||
let buf = buf + "\" width=\"" + pt(w) + "\" height=\"" + pt(body_h)
|
||||
let buf = buf + "\" fill=\"" + fill + "\" stroke=\"" + stroke + "\" stroke-width=\"1.5\"/>\n"
|
||||
// top ellipse
|
||||
let buf = buf + " <ellipse cx=\"" + pt(cx) + "\" cy=\"" + pt(body_y)
|
||||
let buf = buf + "\" rx=\"" + pt(fdiv2(w)) + "\" ry=\"" + pt(ry)
|
||||
let buf = buf + "\" fill=\"" + fill + "\" stroke=\"" + stroke + "\" stroke-width=\"1.5\"/>\n"
|
||||
// bottom ellipse
|
||||
let bot_y: el_val_t = fadd(y, h)
|
||||
let buf = buf + " <ellipse cx=\"" + pt(cx) + "\" cy=\"" + pt(bot_y)
|
||||
let buf = buf + "\" rx=\"" + pt(fdiv2(w)) + "\" ry=\"" + pt(ry)
|
||||
let buf = buf + "\" fill=\"" + fill + "\" stroke=\"" + stroke + "\" stroke-width=\"1.5\"/>\n"
|
||||
}
|
||||
if str_eq(shape, "diamond") {
|
||||
let hw: el_val_t = fdiv2(w)
|
||||
let hh: el_val_t = fdiv2(h)
|
||||
let buf = buf + " <polygon points=\""
|
||||
let buf = buf + pt(cx) + "," + pt(fsub(cy, hh)) + " "
|
||||
let buf = buf + pt(fadd(cx, hw)) + "," + pt(cy) + " "
|
||||
let buf = buf + pt(cx) + "," + pt(fadd(cy, hh)) + " "
|
||||
let buf = buf + pt(fsub(cx, hw)) + "," + pt(cy)
|
||||
let buf = buf + "\" fill=\"" + fill + "\" stroke=\"" + stroke + "\" stroke-width=\"1.5\"/>\n"
|
||||
}
|
||||
|
||||
// Label.
|
||||
let label: String = node["label"]
|
||||
let buf = buf + " <text x=\"" + pt(cx) + "\" y=\"" + pt(cy)
|
||||
let buf = buf + "\" text-anchor=\"middle\" dominant-baseline=\"middle\""
|
||||
let buf = buf + " class=\"arbor-node-label\" fill=\"" + text_col + "\">"
|
||||
let buf = buf + esc(label) + "</text>\n"
|
||||
|
||||
// Sublabel — Rust's DiagramNode stores Option<String>; 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 + " <text x=\"" + pt(cx) + "\" y=\"" + pt(sub_y)
|
||||
let buf = buf + "\" text-anchor=\"middle\" dominant-baseline=\"middle\""
|
||||
let buf = buf + " class=\"arbor-node-label\" fill=\"" + text_col + "\" font-size=\"10\">"
|
||||
let buf = buf + esc(sub) + "</text>\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<String, Any>, layout: Map<String, Any>, forbidden: [String]) -> String {
|
||||
let from_id: String = edge["from"]
|
||||
let to_id: String = edge["to"]
|
||||
let from_pos: Map<String, Any> = el_map_get(layout, "node_pos_" + from_id)
|
||||
let to_pos: Map<String, Any> = 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 + " <line x1=\"" + pt(fx) + "\" y1=\"" + pt(fy)
|
||||
let buf = buf + "\" x2=\"" + pt(tx) + "\" y2=\"" + pt(ty)
|
||||
let buf = buf + "\" stroke=\"" + stroke + "\" stroke-width=\"1.5\""
|
||||
let buf = buf + dash_attr + marker_start + marker_end + "/>\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 + " <circle cx=\"" + pt(mx) + "\" cy=\"" + pt(my)
|
||||
let buf = buf + "\" r=\"" + pt(r) + "\" fill=\"white\" stroke=\""
|
||||
let buf = buf + col_edge_forbidden() + "\" stroke-width=\"1.5\"/>\n"
|
||||
let off: el_val_t = int_to_float(4)
|
||||
let buf = buf + " <line x1=\"" + pt(fsub(mx, off)) + "\" y1=\"" + pt(fsub(my, off))
|
||||
let buf = buf + "\" x2=\"" + pt(fadd(mx, off)) + "\" y2=\"" + pt(fadd(my, off))
|
||||
let buf = buf + "\" stroke=\"" + col_edge_forbidden() + "\" stroke-width=\"1.5\"/>\n"
|
||||
let buf = buf + " <line x1=\"" + pt(fadd(mx, off)) + "\" y1=\"" + pt(fsub(my, off))
|
||||
let buf = buf + "\" x2=\"" + pt(fsub(mx, off)) + "\" y2=\"" + pt(fadd(my, off))
|
||||
let buf = buf + "\" stroke=\"" + col_edge_forbidden() + "\" stroke-width=\"1.5\"/>\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 + " <rect x=\"" + pt(fsub(mx, fdiv2(lw))) + "\" y=\"" + pt(fsub(my, fdiv2(lh)))
|
||||
let buf = buf + "\" width=\"" + pt(lw) + "\" height=\"" + pt(lh)
|
||||
let buf = buf + "\" rx=\"3\" fill=\"white\" opacity=\"0.85\"/>\n"
|
||||
let buf = buf + " <text x=\"" + pt(mx) + "\" y=\"" + pt(my)
|
||||
let buf = buf + "\" text-anchor=\"middle\" dominant-baseline=\"middle\""
|
||||
let buf = buf + " class=\"arbor-edge-label\">" + esc(label) + "</text>\n"
|
||||
}
|
||||
buf
|
||||
}
|
||||
|
||||
// ── Group rendering ────────────────────────────────────────────────────────
|
||||
|
||||
fn render_group(buf: String, group: Map<String, Any>, layout: Map<String, Any>) -> String {
|
||||
let gid: String = group["id"]
|
||||
let bounds: Map<String, Any> = 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 + " <rect x=\"" + pt(bx) + "\" y=\"" + pt(by)
|
||||
let buf = buf + "\" width=\"" + pt(bw) + "\" height=\"" + pt(bh)
|
||||
let buf = buf + "\" rx=\"8\" fill=\"" + col_group_fill() + "\" stroke=\""
|
||||
let buf = buf + col_group_stroke() + "\" stroke-width=\"1\" stroke-dasharray=\"4,3\"/>\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 + " <text x=\"" + pt(lx) + "\" y=\"" + pt(ly)
|
||||
let buf = buf + "\" class=\"arbor-group-label\">" + esc(label) + "</text>\n"
|
||||
buf
|
||||
}
|
||||
|
||||
// ── Public entry point ─────────────────────────────────────────────────────
|
||||
|
||||
fn arbor_render_svg(graph: Map<String, Any>, layout: Map<String, Any>, forbidden: [String]) -> String {
|
||||
let canvas: Map<String, Any> = el_map_get(layout, "canvas")
|
||||
let cw: el_val_t = canvas["w"]
|
||||
let ch: el_val_t = canvas["h"]
|
||||
|
||||
let buf = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"" + pt(cw)
|
||||
let buf = buf + "\" height=\"" + pt(ch) + "\" viewBox=\"0 0 " + pt(cw) + " " + pt(ch) + "\">\n"
|
||||
let buf = buf + " <defs>"
|
||||
let buf = buf + arrow_defs()
|
||||
let buf = buf + "\n <style>\n"
|
||||
let buf = buf + " .arbor-node-label { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 13px; }\n"
|
||||
let buf = buf + " .arbor-group-label { font-family: 'Helvetica Neue', Helvetica, Arial, monospace; font-size: 10px; fill: " + col_group_text() + "; letter-spacing: 0.08em; }\n"
|
||||
let buf = buf + " .arbor-edge-label { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 11px; fill: " + col_edge_label() + "; }\n"
|
||||
let buf = buf + " </style>\n"
|
||||
let buf = buf + " </defs>\n"
|
||||
|
||||
// Groups first (behind everything).
|
||||
let buf = buf + " <!-- Groups -->\n"
|
||||
let groups: [Map<String, Any>] = graph["groups"]
|
||||
let gn: Int = el_list_len(groups)
|
||||
let i = 0
|
||||
while i < gn {
|
||||
let g: Map<String, Any> = 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 + " <!-- Edges -->\n"
|
||||
let edges: [Map<String, Any>] = graph["edges"]
|
||||
let en: Int = el_list_len(edges)
|
||||
let i = 0
|
||||
while i < en {
|
||||
let e: Map<String, Any> = get(edges, i)
|
||||
let buf = render_edge(buf, e, layout, forbidden)
|
||||
let i = i + 1
|
||||
}
|
||||
|
||||
// Nodes
|
||||
let buf = buf + " <!-- Nodes -->\n"
|
||||
let nodes: [Map<String, Any>] = graph["nodes"]
|
||||
let nn: Int = el_list_len(nodes)
|
||||
let i = 0
|
||||
while i < nn {
|
||||
let n: Map<String, Any> = 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 + " <text x=\"" + pt(title_x) + "\" y=\"22\" text-anchor=\"middle\""
|
||||
let buf = buf + " font-family=\"'Helvetica Neue', Helvetica, Arial, sans-serif\""
|
||||
let buf = buf + " font-size=\"15\" font-weight=\"600\" fill=\"" + col_node_text() + "\">"
|
||||
let buf = buf + esc(title) + "</text>\n"
|
||||
}
|
||||
|
||||
let buf = buf + "</svg>\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<String, Any>, layout: Map<String, Any>, forbidden: [String]) -> Map<String, Any> {
|
||||
{
|
||||
"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<String, Any> {
|
||||
{
|
||||
"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<String, Any> {
|
||||
{
|
||||
"from": src, "to": dst, "label": label,
|
||||
"line": line, "arrow": arrow
|
||||
}
|
||||
}
|
||||
|
||||
fn make_test_pos(x: Int, y: Int) -> Map<String, Any> {
|
||||
{ "x": int_to_float(x), "y": int_to_float(y) }
|
||||
}
|
||||
|
||||
fn make_test_size(w: Int, h: Int) -> Map<String, Any> {
|
||||
{ "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<String, Any> {
|
||||
let r: Map<String, Any> = 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<String, Any> = make_test_node("a", "Node A", "rectangle")
|
||||
let n_b: Map<String, Any> = make_test_node("b", "Node B", "rectangle")
|
||||
let e_ab: Map<String, Any> = make_test_edge("a", "b", "solid", "forward", "")
|
||||
|
||||
let nodes: [Map<String, Any>] = native_list_empty()
|
||||
let nodes = native_list_append(nodes, n_a)
|
||||
let nodes = native_list_append(nodes, n_b)
|
||||
let edges: [Map<String, Any>] = native_list_empty()
|
||||
let edges = native_list_append(edges, e_ab)
|
||||
let groups: [Map<String, Any>] = native_list_empty()
|
||||
|
||||
let g: Map<String, Any> = {
|
||||
"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<String, Any> = 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", svg, "<svg xmlns=")
|
||||
check_contains("svg ends with </svg>", svg, "</svg>")
|
||||
check_contains("svg contains node label", svg, "Node A")
|
||||
check_contains("svg contains title", svg, ">Test</text>")
|
||||
check_contains("svg has rect for rectangle node", svg, "<rect")
|
||||
check_contains("svg has line for edge", svg, "<line")
|
||||
check_contains("svg has arrow marker def", svg, "id=\"ah\"")
|
||||
|
||||
// Escape test
|
||||
let n_esc: Map<String, Any> = make_test_node("x", "A & B <C>", "rectangle")
|
||||
let nodes2: [Map<String, Any>] = native_list_empty()
|
||||
let nodes2 = native_list_append(nodes2, n_esc)
|
||||
let g2: Map<String, Any> = {
|
||||
"title": "Test <Title>", "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")
|
||||
}
|
||||
Reference in New Issue
Block a user