Skip to content

Commit eefa005

Browse files
authored
Merge pull request #21 from stlab:sean-parent/flow-arrows
feat: flow arrows and source-cell optimization in begin demo
2 parents 143a7f3 + 93347a1 commit eefa005

9 files changed

Lines changed: 1544 additions & 36 deletions

File tree

.vscode/tasks.json

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,49 @@
11
{
22
"version": "2.0.0",
33
"tasks": [
4+
// ============ Begin Tasks ============
5+
{
6+
"label": "begin: serve (desktop)",
7+
"type": "shell",
8+
"command": "dx",
9+
"args": [
10+
"serve",
11+
"--platform",
12+
"desktop"
13+
],
14+
"options": {
15+
"cwd": "${workspaceFolder}/begin"
16+
},
17+
"problemMatcher": [
18+
"$rustc"
19+
],
20+
"presentation": {
21+
"reveal": "always",
22+
"panel": "dedicated"
23+
},
24+
"isBackground": true
25+
},
26+
{
27+
"label": "begin: serve (web)",
28+
"type": "shell",
29+
"command": "dx",
30+
"args": [
31+
"serve",
32+
"--platform",
33+
"web"
34+
],
35+
"options": {
36+
"cwd": "${workspaceFolder}/begin"
37+
},
38+
"problemMatcher": [
39+
"$rustc"
40+
],
41+
"presentation": {
42+
"reveal": "always",
43+
"panel": "dedicated"
44+
},
45+
"isBackground": true
46+
},
447
// ============ Build Tasks ============
548
{
649
"label": "cargo build",

CLAUDE.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,11 @@ cargo test --doc --workspace
2727
cargo test --workspace <test_name>
2828

2929
# Lint (warnings are errors)
30-
cargo clippy --workspace -- -D warnings
31-
cargo clippy --fix --workspace
30+
# The begin crate is excluded from the workspace commands and checked separately
31+
# with --no-default-features to avoid platform-specific renderer dependencies.
32+
cargo clippy --workspace --exclude begin -- -D warnings
33+
cargo clippy -p begin --no-default-features -- -D warnings
34+
cargo clippy --fix --workspace --exclude begin
3235

3336
# Docs
3437
cargo doc --lib --no-deps --open --workspace
@@ -53,8 +56,8 @@ git checkout -b <username>/<feature-name>
5356

5457
Never commit directly to `main`.
5558

56-
Before creating a PR, run the full check suite locally (fmt, clippy, and test from the
57-
Commands section above).
59+
Before creating a PR, run the full check suite locally — every command in the Commands
60+
section above, including both clippy invocations (workspace and begin).
5861

5962
## Project Status
6063

begin/assets/graph.js

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,25 @@
4141
.attr('height', '100%')
4242
.attr('viewBox', [0, 0, width, height]);
4343

44-
// Arrowhead marker reserved for method-direction arrows
4544
var defs = svg.append('defs');
45+
46+
// Arrow tip at relationship circle edge: refX = tip(10) + REL_R
47+
defs.append('marker')
48+
.attr('id', 'arrowhead-to-rel')
49+
.attr('viewBox', '0 -5 10 10')
50+
.attr('refX', 10 + REL_R).attr('refY', 0)
51+
.attr('markerWidth', 8).attr('markerHeight', 8)
52+
.attr('markerUnits', 'userSpaceOnUse')
53+
.attr('orient', 'auto')
54+
.append('path').attr('d', 'M0,-5L10,0L0,5').attr('fill', '#999');
55+
56+
// Arrow tip at cell rect edge (approx): refX = tip(10) + CELL_W / 2
4657
defs.append('marker')
47-
.attr('id', 'arrowhead')
58+
.attr('id', 'arrowhead-to-cell')
4859
.attr('viewBox', '0 -5 10 10')
49-
.attr('refX', 20).attr('refY', 0)
50-
.attr('markerWidth', 6).attr('markerHeight', 6)
60+
.attr('refX', 10 + CELL_W / 2).attr('refY', 0)
61+
.attr('markerWidth', 8).attr('markerHeight', 8)
62+
.attr('markerUnits', 'userSpaceOnUse')
5163
.attr('orient', 'auto')
5264
.append('path').attr('d', 'M0,-5L10,0L0,5').attr('fill', '#999');
5365

@@ -76,9 +88,9 @@
7688
if (!svg) return;
7789

7890
// Preserve existing node positions by merging into incoming data
79-
var nodeMap = new Map(nodes.map(function (n) { return [n.id, n]; }));
91+
var oldNodeMap = new Map(nodes.map(function (n) { return [n.id, n]; }));
8092
nodes = data.nodes.map(function (n) {
81-
var existing = nodeMap.get(n.id);
93+
var existing = oldNodeMap.get(n.id);
8294
if (existing) {
8395
existing.kind = n.kind;
8496
existing.label = n.label;
@@ -87,6 +99,7 @@
8799
}
88100
return Object.assign({}, n);
89101
});
102+
var nodeMap = new Map(nodes.map(function (n) { return [n.id, n]; }));
90103
links = data.links.map(function (l) { return Object.assign({}, l); });
91104

92105
var changedSet = new Set(data.changed || []);
@@ -101,7 +114,16 @@
101114
return src + '-' + tgt;
102115
})
103116
.join('line')
104-
.attr('class', 'link');
117+
.attr('class', 'link')
118+
.attr('marker-end', function (d) {
119+
if (!data.arrows) return null;
120+
var tgtId = typeof d.target === 'object' ? d.target.id : d.target;
121+
var tgtNode = nodeMap.get(tgtId);
122+
if (!tgtNode) return null;
123+
return tgtNode.kind === 'Cell'
124+
? 'url(#arrowhead-to-cell)'
125+
: 'url(#arrowhead-to-rel)';
126+
});
105127

106128
// Join cell rects
107129
cellLayer.selectAll('rect')

begin/src/app.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,22 +23,34 @@ pub fn make_demo_sheet() -> (Sheet, Labels) {
2323
let a = sheet.add_cell(2.0_f64);
2424
let b = sheet.add_cell(3.0_f64);
2525

26-
let rel = sheet
26+
sheet
2727
.add_relationship(vec![
2828
Method::from_fn_2_1([a, b], c, |x: &f64, y: &f64| Ok(x * y)),
2929
Method::from_fn_2_1([b, c], a, |x: &f64, y: &f64| Ok(y / x)),
3030
Method::from_fn_2_1([a, c], b, |x: &f64, y: &f64| Ok(y / x)),
3131
])
3232
.unwrap();
3333

34+
let d = sheet.add_cell(4.0_f64);
35+
let e = sheet.add_cell(5.0_f64);
36+
37+
sheet
38+
.add_relationship(vec![
39+
Method::from_fn_2_1([d, e], c, |x: &f64, y: &f64| Ok(x * y)),
40+
Method::from_fn_2_1([c, e], d, |x: &f64, y: &f64| Ok(x / y)),
41+
Method::from_fn_2_1([c, d], e, |x: &f64, y: &f64| Ok(x / y)),
42+
])
43+
.unwrap();
44+
3445
// Compute c = a × b = 6 on startup; clear changed so c does not pulse immediately.
3546
sheet.propagate().unwrap();
3647
sheet.clear_changed();
3748

3849
labels.add_cell::<f64>(a, "a");
3950
labels.add_cell::<f64>(b, "b");
4051
labels.add_cell::<f64>(c, "c");
41-
labels.add_relationship(rel, "×");
52+
labels.add_cell::<f64>(d, "d");
53+
labels.add_cell::<f64>(e, "e");
4254

4355
(sheet, labels)
4456
}

begin/src/bridge.rs

Lines changed: 108 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44
//! with stable [`CellId`] and [`RelationshipId`] keys. [`to_graph_data`] serializes a
55
//! [`Sheet`] and its [`Labels`] into a [`GraphData`] value ready for JSON encoding.
66
7-
use std::collections::HashMap;
8-
97
use indexmap::IndexMap;
108
use property_model::{CellId, Error, RelationshipId, Sheet};
119
use serde::Serialize;
@@ -28,16 +26,13 @@ pub struct CellMeta {
2826
pub struct Labels {
2927
/// Cells in insertion order (preserves sidebar ordering).
3028
pub cells: IndexMap<CellId, CellMeta>,
31-
/// Relationship labels (reserved for future tooltip display).
32-
pub relationships: HashMap<RelationshipId, String>,
3329
}
3430

3531
impl Labels {
3632
/// Creates an empty label set.
3733
pub fn new() -> Self {
3834
Self {
3935
cells: IndexMap::new(),
40-
relationships: HashMap::new(),
4136
}
4237
}
4338

@@ -69,13 +64,6 @@ impl Labels {
6964
},
7065
);
7166
}
72-
73-
/// Registers a label for a relationship.
74-
///
75-
/// - Precondition: `id` is a live relationship in the sheet this `Labels` will be used with.
76-
pub fn add_relationship(&mut self, id: RelationshipId, label: &str) {
77-
self.relationships.insert(id, label.to_owned());
78-
}
7967
}
8068

8169
impl Default for Labels {
@@ -106,7 +94,10 @@ pub struct NodeData {
10694
pub value: String,
10795
}
10896

109-
/// A single edge in the D3 graph (undirected; connects a cell to a relationship).
97+
/// A single edge in the D3 graph.
98+
///
99+
/// When [`GraphData::arrows`] is `false` the edge is undirected; when `true`
100+
/// it is directed from `source` to `target`.
110101
#[derive(Serialize, Clone, PartialEq)]
111102
pub struct LinkData {
112103
pub source: String,
@@ -130,6 +121,9 @@ pub struct GraphData {
130121
pub changed: Vec<String>,
131122
/// Always empty; reserved for future `when`/`otherwise` conditional relationships.
132123
pub groups: Vec<GroupData>,
124+
/// `true` when at least one relationship has a cached plan and links are directed where
125+
/// plans exist; `false` when no plan has been computed.
126+
pub arrows: bool,
133127
}
134128

135129
fn cell_node_id(id: CellId) -> String {
@@ -142,11 +136,17 @@ fn rel_node_id(id: RelationshipId) -> String {
142136

143137
/// Serializes `sheet` and `labels` into a [`GraphData`] snapshot for D3.
144138
///
139+
/// When a plan is cached (`sheet.selected_method` returns `Some`) the links are
140+
/// directed (inputs → relationship → outputs) and [`GraphData::arrows`] is `true`.
141+
/// Otherwise all cells adjacent to the relationship are emitted as undirected
142+
/// source→relationship edges and `arrows` is `false`.
143+
///
145144
/// - Complexity: O(c + r + e) where c is the number of cells, r the number of
146145
/// relationships, and e the number of cell–relationship adjacency pairs.
147146
pub fn to_graph_data(sheet: &Sheet, labels: &Labels) -> GraphData {
148147
let mut nodes = Vec::new();
149148
let mut links = Vec::new();
149+
let mut arrows = false;
150150

151151
for id in sheet.cells() {
152152
let (label, value) = labels
@@ -169,7 +169,26 @@ pub fn to_graph_data(sheet: &Sheet, labels: &Labels) -> GraphData {
169169
label: String::new(),
170170
value: String::new(),
171171
});
172-
if let Some(adj) = sheet.relationship_adj(id) {
172+
173+
if let Some(method_idx) = sheet.selected_method(id) {
174+
arrows = true;
175+
if let Some(inputs) = sheet.method_inputs(id, method_idx) {
176+
for &cell_id in inputs {
177+
links.push(LinkData {
178+
source: cell_node_id(cell_id),
179+
target: rel_node_id(id),
180+
});
181+
}
182+
}
183+
if let Some(outputs) = sheet.method_outputs(id, method_idx) {
184+
for &cell_id in outputs {
185+
links.push(LinkData {
186+
source: rel_node_id(id),
187+
target: cell_node_id(cell_id),
188+
});
189+
}
190+
}
191+
} else if let Some(adj) = sheet.relationship_adj(id) {
173192
for &cell_id in adj {
174193
links.push(LinkData {
175194
source: cell_node_id(cell_id),
@@ -186,6 +205,7 @@ pub fn to_graph_data(sheet: &Sheet, labels: &Labels) -> GraphData {
186205
links,
187206
changed,
188207
groups: vec![],
208+
arrows,
189209
}
190210
}
191211

@@ -205,12 +225,11 @@ mod tests {
205225
let c = sheet.add_cell(0.0_f64);
206226
labels.add_cell::<f64>(c, "c");
207227

208-
let rel = sheet
228+
sheet
209229
.add_relationship(vec![Method::from_fn_2_1([a, b], c, |x: &f64, y: &f64| {
210230
Ok(x * y)
211231
})])
212232
.unwrap();
213-
labels.add_relationship(rel, "×");
214233

215234
(sheet, labels)
216235
}
@@ -296,6 +315,79 @@ mod tests {
296315
assert!(data.groups.is_empty());
297316
}
298317

318+
// Separate helper that adds the output cell first so propagation succeeds.
319+
fn demo_sheet_with_plan() -> (Sheet, Labels) {
320+
let mut sheet = Sheet::new();
321+
let mut labels = Labels::new();
322+
323+
// c added first → lowest strength (output by default).
324+
let c = sheet.add_cell(0.0_f64);
325+
labels.add_cell::<f64>(c, "c");
326+
let a = sheet.add_cell(2.0_f64);
327+
labels.add_cell::<f64>(a, "a");
328+
let b = sheet.add_cell(3.0_f64);
329+
labels.add_cell::<f64>(b, "b");
330+
331+
sheet
332+
.add_relationship(vec![Method::from_fn_2_1([a, b], c, |x: &f64, y: &f64| {
333+
Ok(x * y)
334+
})])
335+
.unwrap();
336+
337+
(sheet, labels)
338+
}
339+
340+
#[test]
341+
fn to_graph_data_arrows_false_before_propagate() {
342+
let (sheet, labels) = demo_sheet_with_plan();
343+
let data = to_graph_data(&sheet, &labels);
344+
assert!(!data.arrows);
345+
}
346+
347+
#[test]
348+
fn to_graph_data_arrows_true_after_propagate() {
349+
let (mut sheet, labels) = demo_sheet_with_plan();
350+
sheet.propagate().unwrap();
351+
let data = to_graph_data(&sheet, &labels);
352+
assert!(data.arrows);
353+
}
354+
355+
#[test]
356+
fn to_graph_data_directed_input_links_target_relationship() {
357+
// Method [a, b] → c; after propagate, a and b are inputs → 2 edges into rel.
358+
let (mut sheet, labels) = demo_sheet_with_plan();
359+
sheet.propagate().unwrap();
360+
let data = to_graph_data(&sheet, &labels);
361+
362+
let rel_id = data
363+
.nodes
364+
.iter()
365+
.find(|n| n.kind == NodeKind::Relationship)
366+
.map(|n| n.id.clone())
367+
.unwrap();
368+
369+
let to_rel: Vec<_> = data.links.iter().filter(|l| l.target == rel_id).collect();
370+
assert_eq!(to_rel.len(), 2);
371+
}
372+
373+
#[test]
374+
fn to_graph_data_directed_output_links_source_relationship() {
375+
// Method [a, b] → c; after propagate, c is the output → 1 edge out of rel.
376+
let (mut sheet, labels) = demo_sheet_with_plan();
377+
sheet.propagate().unwrap();
378+
let data = to_graph_data(&sheet, &labels);
379+
380+
let rel_id = data
381+
.nodes
382+
.iter()
383+
.find(|n| n.kind == NodeKind::Relationship)
384+
.map(|n| n.id.clone())
385+
.unwrap();
386+
387+
let from_rel: Vec<_> = data.links.iter().filter(|l| l.source == rel_id).collect();
388+
assert_eq!(from_rel.len(), 1);
389+
}
390+
299391
#[test]
300392
fn display_closure_returns_value_string() {
301393
let (sheet, labels) = demo_sheet();

0 commit comments

Comments
 (0)