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
15 changes: 14 additions & 1 deletion engraphis/dashboard_assets/engraphis-graph.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -9320,10 +9320,20 @@
fg.linkDirectionalArrowLength(dense ? 0 : 0.625).linkDirectionalArrowRelPos(1);
applyLinkLabels();
if (fg.linkDirectionalParticles) {
const rawFlowSpeed = 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). When a standalone caller omits flowSpeed
(Number(undefined) → NaN), fall back to the historical default of 45 so the
speed formula below stays finite and the active check stays meaningful. */
const flowSpeed = Number.isFinite(rawFlowSpeed) ? rawFlowSpeed : 45;
const flowActive = flowSpeed > 0;
const flowing = !fullGraph
&& state.settings.flow !== false
&& motion
&& !reducedMotion
&& flowActive
&& data.links.length <= PARTICLE_LINK_LIMIT;
const particles = !flowing
? 0
Expand All@@ -9332,7 +9342,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);
Comment thread
Coding-Dev-Tools marked this conversation as resolved.
}
if (!galaxyMode && reheat && motion && !staticFullLayout && !state.settings.frozen) {
prepareReheat();
Expand Down
10 changes: 10 additions & 0 deletions engraphis/dashboard_assets/ledger.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -2453,6 +2453,16 @@
function graphSliderResponseValue(id, value, baseline) {
const control = byId(id);
if (!control) return Number.isFinite(Number(value)) ? Number(value) : baseline;
/* Flow speed is a linear control: the 2x response centered at 45 would
clamp every visible value in the lower quarter of the slider to
0, which the compat engine then treats as "stop" and the user
sees an inert slider end. Use the raw value for flow-speed and
keep the 2x response for the geometry controls (gravity, repel,
link, etc.) where the centered calibration is intentional. */
if (id === 'graph-flow-speed') {
const rawFlow = graphValueInRange(id, value, baseline);
return Number.isFinite(Number(rawFlow)) ? Number(rawFlow) : baseline;
}
const raw = graphValueInRange(id, value, baseline);
const center = Number.isFinite(Number(baseline)) ? Number(baseline) : raw;
const min = Number(control.min);
Expand Down
2 changes: 1 addition & 1 deletion tests/e2e/graph-engine.spec.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -1937,7 +1937,7 @@ test('served Ledger wires normalized spacetime controls, overlay, and orbit paus
};
});
expect(rangeResponse.settings).toMatchObject({
flowSpeed: 85, repel: 200, link: 32, gravity: 144, size: 5, font: 20,
flowSpeed: 65, repel: 200, link: 32, gravity: 144, size: 5, font: 20,
linkw: 1.28, labelDensity: 56,
});
expect(rangeResponse.scope).toEqual({ minDegree: 2, depth: 3 });
Expand Down
50 changes: 50 additions & 0 deletions tests/test_graph_engine_asset.py
Original file line numberDiff line numberDiff 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