A Rete network as a picture: left to right, with what is known marked on
it. Pure: a network and what is known in, SVG out. It draws the
[Network] that runs, so the picture cannot show rules other than the
ones that run.
Columns: the facts, the tests on them (alpha nodes), the joins of tests (one column per depth), and the rules. A rule's first test is its own first join, so only joins of two or more tests are drawn as nodes.
What the picture has to say, beyond the wiring:
- which facts came in one request (the frames around the facts, which
the host names in its [
Legend]); - what a source actually answered (the text
showngives for a fact); - what, of everything known, a rule that holds stands on (filled), and what was learned and not needed (outlined);
- the order the rules decided in (the number on a rule);
- that a rule's effect teaches a fact (the line from a rule back round to the fact, which is the loop the engine runs).
The markup carries classes and no styles. rete, fact, alpha, join,
rule, frame, back, nth and fired name the parts, and holds,
spare, fails and waits the state of one; [standalone] writes the
picture as a file with the styles a host supplies.
24#![forbid(unsafe_code)]
26use std::collections::BTreeSet; 27 28use maud::{Markup, html}; 29use rete::{Domain, Known, Network, State, Test, Then}; 30 31const ROW: i32 = 28; 32const TOP: i32 = 32; 33const BOX_H: i32 = 18; 34const FACT_X: i32 = 0; 35const FACT_W: i32 = 150; 36const ALPHA_X: i32 = 184; 37const ALPHA_W: i32 = 74; 38const JOIN_X: i32 = 294; 39const JOIN_STEP: i32 = 40; 40const JOIN_R: i32 = 8; 41const RULE_W: i32 = 262;
Room round the network for the lines that run from a rule back to the fact it teaches: one lane per such fact.
A frame's room above its first fact, for its caption, and round its sides.
What only the host can say about its network.
51pub struct Legend<D: Domain> {
The facts that arrive together, and what brings them: a caption for each frame drawn round those facts.
54 pub frames: Vec<(String, Vec<D::Fact>)>,
Something on the diagram that can be clicked.
The first free row at or after y in a column, so two nodes that want
the same place do not sit on each other.
The network with known marked on it. fired is the rules that decided
a step, in order, as indices into the network's terminals. shown is
what to print for a known fact: Jev's own number, where there is one.
link is where a test leads when it is clicked, for the playground.
89pub fn rete<D: Domain>( 90 network: &Network<D>, 91 known: &Known<D>, 92 fired: &[usize], 93 legend: &Legend<D>, 94 shown: impl Fn(D::Fact) -> Option<String>, 95 link: impl Fn(Clicked<D>) -> Option<String>, 96) -> Markup { 97 let linked = |clicked: Clicked<D>, node: Markup| match link(clicked) { 98 Some(href) => html! { a href=(href) { (node) } }, 99 None => node, 100 }; 101 // Alphas in fact order, so a fact's tests sit together beside it. 102 let mut order: Vec<usize> = (0..network.alphas.len()).collect(); 103 order.sort_by_key(|&alpha| network.alphas[alpha].fact()); 104 let mut alpha_y = vec![0; network.alphas.len()]; 105 for (row, &alpha) in order.iter().enumerate() { 106 alpha_y[alpha] = TOP + row as i32 * ROW; 107 } 108 let fact_y = |fact: D::Fact| { 109 let ys: Vec<i32> = 110 (0..network.alphas.len()).filter(|&a| network.alphas[a].fact() == fact).map(|a| alpha_y[a]).collect(); 111 (!ys.is_empty()).then(|| ys.iter().sum::<i32>() / ys.len() as i32) 112 }; 113 114 let depth = network.joins.iter().map(|join| join.depth).max().unwrap_or(1); 115 let join_x = |d: usize| JOIN_X + (d as i32 - 2) * JOIN_STEP; 116 let rule_x = join_x(depth) + JOIN_STEP; 117 let width = rule_x + RULE_W; 118 let height = TOP + network.alphas.len() as i32 * ROW - ROW / 2; 119 120 // Where each join's output leaves from: an alpha's right edge for a 121 // rule's first test, the join's own node otherwise. 122 let mut columns: Vec<BTreeSet<i32>> = vec![BTreeSet::new(); depth + 1]; 123 let mut out: Vec<(i32, i32)> = Vec::with_capacity(network.joins.len()); 124 for join in &network.joins { 125 out.push(if join.depth == 1 { 126 (ALPHA_X + ALPHA_W, alpha_y[join.alpha]) 127 } else { 128 (join_x(join.depth), place(&mut columns[join.depth], alpha_y[join.alpha])) 129 }); 130 } 131 let mut rule_rows = BTreeSet::new(); 132 let rule_y: Vec<i32> = network.terminals.iter().map(|t| place(&mut rule_rows, out[t.join].1)).collect(); 133 134 let (used_joins, used_alphas) = network.used(known); 135 let alpha_class = |alpha: usize| class(network.alpha(alpha, known), used_alphas[alpha]); 136 let join_class = |join: usize| class(network.join(join, known), used_joins[join]); 137 let fact_class = |fact: D::Fact| match known.get(fact) { 138 None => "waits", 139 Some(_) if (0..network.alphas.len()).any(|a| network.alphas[a].fact() == fact && used_alphas[a]) => "holds", 140 Some(_) => "spare", 141 }; 142 143 // Each effect's fact has a lane: out to the right of its rules, under 144 // the network, and up the left to the fact. 145 let taught: Vec<D::Fact> = D::facts().iter().copied().filter(|fact| !D::asked_for(*fact)).collect(); 146 let lane = |fact: D::Fact| taught.iter().position(|taught| *taught == fact).map(|lane| 10 + lane as i32 * LANE); 147 148 html! { 149 div .rete { 150 svg viewBox={ (-GUTTER) " 0 " (width + 2 * GUTTER) " " (height + GUTTER) } role="img" 151 aria-label="The rules as a Rete network" { 152 // Frames and lines first, so the nodes are drawn over them. 153 @for (caption, facts) in &legend.frames { 154 @let ys: Vec<i32> = facts.iter().filter_map(|fact| fact_y(*fact)).collect(); 155 @if let (Some(top), Some(bottom)) = (ys.iter().min(), ys.iter().max()) { 156 @let top = top - BOX_H / 2 - CAPTION; 157 g .frame { 158 rect x=(FACT_X - PAD) y=(top) width=(FACT_W + 2 * PAD) 159 height=(bottom + BOX_H / 2 + PAD - top) {} 160 text x=(FACT_X) y=(top + 11) { (caption) } 161 } 162 } 163 } 164 @for (alpha, test) in network.alphas.iter().enumerate() { 165 @if let Some(y) = fact_y(test.fact()) { 166 line class=(alpha_class(alpha)) x1=(FACT_X + FACT_W + PAD) y1=(y) x2=(ALPHA_X) y2=(alpha_y[alpha]) {} 167 } 168 } 169 // The lines into a join that a fired rule stands on are drawn 170 // last, over the ones beside them: several joins share the 171 // line that brings what came before. 172 @for last in [false, true] { 173 @for (index, join) in network.joins.iter().enumerate() { 174 @if let (Some(left), true) = (join.left, used_joins[index] == last) { 175 @let (x, y) = out[index]; 176 @let (from_x, from_y) = out[left]; 177 @let from_x = if network.joins[left].depth == 1 { from_x } else { from_x + JOIN_R }; 178 @let edge = if from_y < y { y - JOIN_R } else { y + JOIN_R }; 179 // What came before arrives from above or below; 180 // the test joined here arrives from the left. 181 path class=(class(network.join(left, known), last)) 182 d={ "M" (from_x) " " (from_y) " H" (x) " V" (edge) } {} 183 line class=(class(network.alpha(join.alpha, known), last)) 184 x1=(ALPHA_X + ALPHA_W) y1=(alpha_y[join.alpha]) x2=(x - JOIN_R) y2=(y) {} 185 } 186 } 187 } 188 @for (terminal, y) in network.terminals.iter().zip(&rule_y) { 189 @let (x, from_y) = out[terminal.join]; 190 @let x = if network.joins[terminal.join].depth == 1 { x } else { x + JOIN_R }; 191 line class=(join_class(terminal.join)) x1=(x) y1=(from_y) x2=(rule_x) y2=(*y) {} 192 @if let Then::Do(effect) = terminal.then { 193 @let fact = D::teaches(effect); 194 @if let (Some(to), Some(lane)) = (fact_y(fact), lane(fact)) { 195 @let tip = FACT_X - PAD; 196 g class={ "back " (join_class(terminal.join)) } { 197 path d={ 198 "M" (width) " " (*y) " H" (width + lane) " V" (height + lane) 199 " H" (-lane - PAD) " V" (to) " H" (tip - 5) 200 } {} 201 polygon points={ (tip) "," (to) " " (tip - 6) "," (to - 3) " " (tip - 6) "," (to + 3) } {} 202 } 203 } 204 } 205 } 206 207 @for fact in D::facts().iter().copied() { 208 @if let Some(y) = fact_y(fact) { 209 (linked(Clicked::Fact(fact), html! { 210 g class={ "fact " (fact_class(fact)) } { 211 rect x=(FACT_X) y=(y - BOX_H / 2) width=(FACT_W) height=(BOX_H) {} 212 text x=(FACT_X + 6) y=(y + 4) { (D::fact_name(fact)) } 213 @if let Some(value) = known.get(fact) { 214 text x=(FACT_X + FACT_W - 6) y=(y + 4) text-anchor="end" { 215 (shown(fact).unwrap_or_else(|| D::value_name(value).to_owned())) 216 } 217 } 218 } 219 })) 220 } 221 } 222 @for (alpha, test) in network.alphas.iter().enumerate() { 223 (linked(Clicked::Test(*test), html! { 224 g class={ "alpha " (alpha_class(alpha)) } { 225 rect x=(ALPHA_X) y=(alpha_y[alpha] - BOX_H / 2) width=(ALPHA_W) height=(BOX_H) {} 226 text x=(ALPHA_X + 6) y=(alpha_y[alpha] + 4) { (test.label()) } 227 } 228 })) 229 } 230 @for (index, join) in network.joins.iter().enumerate() { 231 @if join.depth > 1 { 232 g class={ "join " (join_class(index)) } { 233 circle cx=(out[index].0) cy=(out[index].1) r=(JOIN_R) {} 234 text x=(out[index].0) y=(out[index].1 + 4) text-anchor="middle" { "&" } 235 } 236 } 237 } 238 @for (index, (terminal, y)) in network.terminals.iter().zip(&rule_y).enumerate() { 239 @let nth = fired.iter().position(|fired| *fired == index); 240 g class={ "rule " (join_class(terminal.join)) @if nth.is_some() { " fired" } } { 241 rect x=(rule_x) y=(y - BOX_H / 2) width=(RULE_W) height=(BOX_H) {} 242 text x=(rule_x + 6) y=(y + 4) { (terminal.name) " → " ((legend.then)(terminal.then)) } 243 @if let Some(nth) = nth { 244 rect .nth x=(width - BOX_H) y=(y - BOX_H / 2) width=(BOX_H) height=(BOX_H) {} 245 text .nth x=(width - BOX_H / 2) y=(y + 4) text-anchor="middle" { (nth + 1) } 246 } 247 } 248 } 249 } 250 } 251 } 252}
The rules that decided a step, in order, numbered, for a title bar.
The network on its own, as an SVG file, with nothing known so every node
waits. style is the stylesheet for the classes above, written for the
file: an image is drawn without the page's stylesheet, so it comes with
the picture.
265pub fn standalone<D: Domain>(network: &Network<D>, legend: &Legend<D>, style: &str) -> String { 266 let drawn = rete(network, &Known::default(), &[], legend, |_| None, |_| None).into_string(); 267 let svg = drawn.trim_start_matches(r#"<div class="rete">"#).trim_end_matches("</div>"); 268 svg.replacen("<svg ", r#"<svg xmlns="http://www.w3.org/2000/svg" "#, 1).replacen( 269 r#"aria-label="The rules as a Rete network">"#, 270 &format!(r#"aria-label="The rules as a Rete network"><style>{style}</style>"#), 271 1, 272 ) 273}