Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion engraphis/dashboard_assets/engraphis-graph.js
Original file line number Diff line number Diff line change
Expand Up @@ -9320,10 +9320,17 @@
fg.linkDirectionalArrowLength(dense ? 0 : 0.625).linkDirectionalArrowRelPos(1);
applyLinkLabels();
if (fg.linkDirectionalParticles) {
const flowSpeed = Number(state.settings.flowSpeed);
/* flowSpeed=0 means "stop" — particles must not render at all. The every-node engine
already enforces this via a `moving = speed > 0` check; the compat engine must do
the same. Otherwise the slider visibly does nothing at the low end (particles keep
crawling at the residual 0.002 floor). */
const flowActive = Number.isFinite(flowSpeed) ? flowSpeed > 0 : true;
const flowing = !fullGraph
&& state.settings.flow !== false
&& motion
&& !reducedMotion
&& flowActive
Comment on lines +9328 to +9333

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Stop flow only at the slider's visible zero endpoint

In the dashboard, visible slider values 1 through 22 are transformed to an engine value of 0 by ledger.js:2453-2462, because the 2x response is centered at 45 and clamped at the minimum. This new guard therefore removes particles throughout roughly the lower quarter of the control, not just when the user selects zero, making that portion of the slider inert. The zero-stop decision needs to preserve the visible endpoint separately or avoid applying the centered response mapping to flow speed.

Useful? React with 👍 / 👎.

&& data.links.length <= PARTICLE_LINK_LIMIT;
const particles = !flowing
? 0
Expand All @@ -9332,7 +9339,10 @@
.linkDirectionalParticleWidth(1)
.linkDirectionalParticleCanvasObject(paintFlowArrow)
.linkDirectionalParticleColor(l => alpha(layerColor(l.layer), 0.95))
.linkDirectionalParticleSpeed(l => 0.002 + ((state.settings.flowSpeed || 45) / 100) * 0.008);
/* Widened from `0.002 + (flowSpeed/100)*0.008` (a 5x range, 0.002..0.01) to
`0.0005 + (flowSpeed/100)*0.025` (a ~34x range, 0.00075..0.0255) so the slider
is visibly responsive end-to-end. */
.linkDirectionalParticleSpeed(l => flowActive ? (0.0005 + (flowSpeed / 100) * 0.025) : 0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the default speed when flowSpeed is unset

When a standalone caller uses the exported EngraphisGraph.create() API and calls setData() without first supplying flowSpeed, the initial settings at engraphis-graph.js:7357-7365 leave it undefined. flowActive then defaults to true, but this callback computes with NaN, so active links receive three particles with an unusable particle speed instead of the previous default based on 45. Use a finite fallback speed before both the activity check and speed calculation.

Useful? React with 👍 / 👎.

}
if (!galaxyMode && reheat && motion && !staticFullLayout && !state.settings.frozen) {
prepareReheat();
Expand Down
50 changes: 50 additions & 0 deletions tests/test_graph_engine_asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -10631,6 +10631,56 @@ def test_physics_sliders_reheat_the_simulation_the_way_the_classic_renderer_does
assert report["reducedMotion"] == 1, "reduced motion silently disabled live physics"


@requires_node
def test_flow_speed_slider_has_a_visible_range_in_compat_engine() -> None:
"""The compat engine's flow-speed slider must produce a visibly larger particle speed at
the top of the range than at the bottom. Earlier the speed formula
`0.002 + (flowSpeed/100)*0.008` produced a 5x range (0.002..0.01) that was too small to be
noticeable end-to-end. The fix widens that to a 34x range
(`0.0005 + (flowSpeed/100)*0.025` -> 0.00075..0.0255). The test snapshots the per-link
speed closure at the two ends of the slider and asserts the high end is materially
larger than the low end.
"""
report = _run_engine(
"""
const api = G.create(el, {});
api.setPreset('compact');
api.setData(chain(40));
const linkForSample = { layer: 'semantic' };
const sample = (flowSpeed) => {
api.setSettings({ flowSpeed, flow: true });
// linkDirectionalParticleSpeed is a chainable setter; the engine has stored a
// per-link function on the stub. Sample it on a non-suggested, non-ghost link.
const speedFn = store.linkDirectionalParticleSpeed;
return typeof speedFn === 'function' ? speedFn(linkForSample) : null;
};
const low = sample(1);
const mid = sample(50);
const high = sample(100);
const stop = sample(0);
emit({ low, mid, high, stop });
"""
)
# At flowSpeed=0 the engine short-circuits the closure to 0 (particles do not move).
assert report["stop"] == 0, (
f"flowSpeed=0 must yield particle speed 0 (compat engine stop-at-zero), "
f"got {report['stop']}"
)
# At flowSpeed=1 the closure must return a non-zero, low-end value.
assert report["low"] > 0, f"flowSpeed=1 must yield non-zero speed, got {report['low']}"
# End-to-end the slider must show a wide range: high is materially larger than low.
# The fix targets a 34x range; allow some headroom for d3 stub arithmetic.
assert report["high"] >= report["low"] * 10, (
f"flowSpeed slider must produce a visible range (>=10x low-to-high). "
f"low={report['low']} high={report['high']}"
)
# Monotonicity: low < mid < high.
assert report["low"] < report["mid"] < report["high"], (
f"flow speed must be monotonic in the slider value: low={report['low']} "
f"mid={report['mid']} high={report['high']}"
)


@requires_node
def test_full_graph_within_the_force_budget_keeps_centre_gravity_live() -> None:
"""Full mode must not turn a normal large workspace into a pinned, inert ring.
Expand Down