Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,6 @@ uv.lock
# mixed-case variants from slipping through on case-insensitive machines.
/[Pp][Rr][Oo][Mm][Pp][Tt]_[Ss][Ee][Ss][Ss][Ii][Oo][Nn]_[Tt][Rr][Aa][Nn][Ss][Cc][Rr][Ii][Pp][Tt]_[Aa][Nn][Aa][Ll][Yy][Ss][Ii][Ss].md
/[Ss][Ee][Ss][Ss][Ii][Oo][Nn]_[Tt][Rr][Aa][Nn][Ss][Cc][Rr][Ii][Pp][Tt]_[Qq][Uu][Ee][Rr][Yy]_[Pp][Rr][Oo][Mm][Pp][Tt].md

# Command Code per-session scratchpad (agent working files, never package content).
/$COMMANDCODE_SCRATCHPAD/
4 changes: 2 additions & 2 deletions engraphis/classic_assets/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<link rel="icon" type="image/x-icon" href="/classic-assets/engraphis.ico">
<link rel="icon" type="image/png" sizes="32x32" href="/classic-assets/favicon-32.png">
<title>Engraphis</title>
<link rel="stylesheet" href="/classic-assets/dashboard.css?v=20260831-galaxy-floor-fix-2">
<link rel="stylesheet" href="/classic-assets/dashboard.css?v=20260902-slider-merge-1">
</head>
<body>
<a class="skip-link" href="#main-content">Skip to main content</a>
Expand Down Expand Up @@ -349,6 +349,6 @@
graph view. dashboard.js fetches both on demand from graphRender(); see loadForceGraph()
and loadGraphEngine(). scripts/externalize_dashboard_assets.py enforces both halves:
they stay out of this file, and the lazy references still have to resolve. -->
<script src="/classic-assets/dashboard.js?v=20260828-galaxy-default-gravity-1"></script>
<script src="/classic-assets/dashboard.js?v=20260902-slider-merge-1"></script>
</body>
</html>
99 changes: 79 additions & 20 deletions engraphis/dashboard_assets/engraphis-graph.js
Original file line number Diff line number Diff line change
Expand Up @@ -7501,6 +7501,7 @@
saved phase byte-for-byte after the render's safety projections. */
let galaxyPhaseRestorePending = false;
let preserveGalaxyPhaseOnResume = false;
let galaxyContactCorrectionDeferred = false;
let adj = Object.create(null), liveAdj = Object.create(null), hilite = null, hoverSet = null, maxDeg = 1;
let legacySizeBy = 'degree';
// The classic renderer treats label density as a hard ranked cap, not merely a looser
Expand Down Expand Up @@ -8040,6 +8041,7 @@
setSimulationBudget(false, true);
}


function applyForces() {
/* Extremely large complete snapshots use the deterministic fallback, but a normal
full graph remains a live layout. The previous `renderMode === 'full'` guard removed
Expand Down Expand Up @@ -8130,15 +8132,16 @@
large) is the neutral settling behaviour, so the slider's effect is a *multiplier*
on that baseline, not a replacement. Above 1 the layout settles harder, below 1
it stays more elastic. */
/* Space friction (the dashboard's "damping" slider) maps onto d3's velocityDecay.
The slider's 0..15 visible range must reach the full d3 decay range so the lower
quarter is not inert. At the default (slider=1) the size-aware baseline (0.38 small
/ 0.45 large) is the neutral settling behaviour, so the slider's effect is a
*multiplier* on that baseline, not a replacement. Above 1 the layout settles
harder, below 1 it stays more elastic, down to the d3 floor 0.05 at 0. */
if (fg.d3VelocityDecay) {
const dampingRaw = Number(state.settings.damping);
const damping = Number.isFinite(dampingRaw) ? clamp(dampingRaw, 0, 15) : 1;
const baseline = large ? 0.45 : 0.38;
/* Linearly interpolate between the d3 velocityDecay floor (0.05) at damping=0,
the size-aware baseline at damping=1, and the d3 velocityDecay ceiling (0.85)
at damping=15. The full 0..15 visible range is now meaningful, and the default
(damping=1) keeps the size-aware settling behaviour the rest of the engine
already assumes. */
const floor = 0.05;
const ceiling = 0.85;
const target = damping <= 1
Expand Down Expand Up @@ -9268,6 +9271,7 @@
behaviour synchronous while browsers coalesce a burst of range-input events. */
if (typeof window === 'undefined' || typeof window.requestAnimationFrame !== 'function') {
physicsReheatPending = false;
galaxyContactCorrectionDeferred = false;
render(false, true);
return;
}
Expand All @@ -9282,7 +9286,13 @@
physicsFrame = 0;
if (destroyed || suspended || !physicsReheatPending) return;
physicsReheatPending = false;
if (phaseLock) preserveGalaxyPhaseOnResume = true;
if (phaseLock) {
preserveGalaxyPhaseOnResume = true;
/* Apply the painted-edge projection once, after the browser has coalesced the
complete input burst. Intermediate projections would make the final layout depend
on how many range-input events happened before this frame. */
galaxyContactCorrectionDeferred = false;
}
render(false, true);
});
}
Expand Down Expand Up @@ -9494,7 +9504,7 @@
envelopeRadius: galaxyLastFarFieldConfinement.envelopeRadius,
softRadius: galaxyLastFarFieldConfinement.softRadius,
samples: 0, acceleratedSystems: 0, acceleratedCoreNodes: 0,
acceleratedFixedFollowers: 0, maximumAcceleration: 0,
acceleratedFixedSource: 0, acceleratedFixedFollowers: 0, maximumAcceleration: 0,
};
const postOuterHorizon = applyGalaxyBlackHoleExclusion(
data.nodes, { padding: GALAXY_BLACK_HOLE_EXCLUSION_PADDING }
Expand All @@ -9516,6 +9526,60 @@
galaxyLastBlackHoleExclusion = combineGalaxyBlackHoleExclusions(
[prePaintHorizon, postOuterHorizon, postStarHorizon]
);
} else if (reused && galaxyMode && skipGalaxyReseed) {
/* The slider burst just rescaled carriers and their satellites by a known ratio.
The phase-preserving contact pass is deferred until the coalesced frame below. */
const skippedAnchor = galaxyGlobalAnchor(data.nodes);
if (galaxyContactCorrectionDeferred) {
/* A range-input burst can issue several synchronous settings updates before the
scheduled browser frame. Preserve the complete multiplicative response first, then
project once in that final frame. This keeps the painted-edge invariant while
making a single jump and an equivalent fine sweep converge to the same state. */
galaxyLastSystemAnchorExclusion = combineGalaxySystemAnchorExclusions([{
padding: GALAXY_SYSTEM_ANCHOR_EXCLUSION_PADDING,
systems: 0, contacts: 0, correctedDistance: 0, maximumShift: 0,
inwardVelocityRemoved: 0, tangentialVelocityRemoved: 0,
minimumClearance: null, iterations: 0,
}]);
galaxyLastBlackHoleExclusion = combineGalaxyBlackHoleExclusions([{
anchorId: skippedAnchor ? skippedAnchor.id : null,
contacts: 0, systems: 0, coreNodes: 0, fixedSystemNodes: 0,
repelledNodes: 0, correctedDistance: 0, maximumShift: 0,
inwardVelocityRemoved: 0, tangentialVelocityRemoved: 0,
minimumClearance: null,
}]);
} else {
/* The final scheduled frame still enforces both painted contact boundaries, including
while the Galaxy clock is frozen or orbit-paused. */
const prePaintHorizon = applyGalaxyBlackHoleExclusion(
data.nodes, { padding: GALAXY_BLACK_HOLE_EXCLUSION_PADDING }
);
const preStarExclusion = applyGalaxySystemAnchorExclusion(data.nodes, {
padding: GALAXY_SYSTEM_ANCHOR_EXCLUSION_PADDING,
fixAnchors: true,
});
const postStarExclusion = applyGalaxySystemAnchorExclusion(data.nodes, {
padding: GALAXY_SYSTEM_ANCHOR_EXCLUSION_PADDING,
fixAnchors: true,
});
const postPaintHorizon = applyGalaxyBlackHoleExclusion(
data.nodes, { padding: GALAXY_BLACK_HOLE_EXCLUSION_PADDING }
);
galaxyLastSystemAnchorExclusion = combineGalaxySystemAnchorExclusions(
[preStarExclusion, postStarExclusion]
);
galaxyLastBlackHoleExclusion = combineGalaxyBlackHoleExclusions(
[prePaintHorizon, postPaintHorizon]
);
}
galaxyLastFarFieldConfinement = galaxyLastFarFieldConfinement || {
anchorId: skippedAnchor ? skippedAnchor.id : null, envelopeRadius: 0, softRadius: 0,
acceleratedSystems: 0, boundedSystems: 0, boundedCoreNodes: 0,
boundedFixedSource: 0, boundedFixedFollowers: 0, boundedDeformedSystems: 0,
boundedOversizedNodes: 0, correctedDistance: 0, maximumShift: 0,
outwardVelocityRemoved: 0, tangentialVelocityRemoved: 0,
annulus: { innerCorrectedNodes: 0, outerCorrectedNodes: 0, infeasibleNodes: 0 },
};
}
applyForces();
fg.autoPauseRedraw(!needsContinuousFrames());
Expand All @@ -9526,19 +9590,6 @@
/* D3 is only the renderer in Galaxy mode. Its alpha, velocity decay and countdown are
intentionally untouched; the fixed-step clock owns all three physical concerns. */
if (!galaxyMode && fg.d3AlphaDecay) fg.d3AlphaDecay(staticFullLayout ? 1 : alphaDecay());
if (!galaxyMode && fg.d3VelocityDecay) {
/* applyForces() above already installed the user-facing damping slider value. The
size-aware baseline (0.38 small / 0.45 large) is only the *default* when the user
has not touched the slider, so this fallback must not clobber a value the user has
already set. The proxy in the test harness (and the real force-graph) returns the
same function for any property access, so we cannot ask "was the setter called?" —
instead we honour the slider's value whenever it is finite, and only fall back to
the size-aware baseline when the dashboard never supplied a damping value. */
const dampingSetting = Number(state.settings.damping);
if (!Number.isFinite(dampingSetting)) {
fg.d3VelocityDecay(large ? 0.45 : 0.38);
}
}
if (fg.linkCurvature) {
fg.linkCurvature(dense ? 0 : ((PRESETS[state.settings.mode] || PRESETS.compact).curve || 0));
}
Expand Down Expand Up @@ -10187,6 +10238,7 @@
|| next.blackHoleMass !== undefined || next.damping !== undefined
|| next.springStiffness !== undefined)) {
preserveGalaxyPhaseOnResume = true;
galaxyContactCorrectionDeferred = true;
}
if (gravityChanged && previousMode === 'galaxy' && state.settings.mode === 'galaxy') {
/* Gravity changes need an immediate, legible density response: a range control whose
Expand Down Expand Up @@ -10271,6 +10323,13 @@
preserveGalaxyPhaseOnResume = true;
}
render(false, false);
/* Re-arm the phase-preserve flag for the synchronous physics reheat that follows. That
reheat shares the same render path and would otherwise run contact corrections on the
post-scaling layout, undoing the slider's burst response and breaking path
independence across burst intermediates. */
if (gravityChanged && previousMode === 'galaxy' && state.settings.mode === 'galaxy') {
preserveGalaxyPhaseOnResume = true;
}
if (layoutChanged) schedulePhysicsUpdate();
};
api.setPreset = name => {
Expand Down
2 changes: 1 addition & 1 deletion engraphis/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,6 @@
graph view. dashboard.js fetches both on demand from graphRender(); see loadForceGraph()
and loadGraphEngine(). scripts/externalize_dashboard_assets.py enforces both halves:
they stay out of this file, and the lazy references still have to resolve. -->
<script src="/static/dashboard.js?v=20260828-galaxy-default-gravity-1"></script>
<script src="/static/dashboard.js?v=20260902-slider-merge-1"></script>
</body>
</html>
42 changes: 42 additions & 0 deletions tests/e2e/graph-engine.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2783,6 +2783,48 @@ test('served primary dashboard keeps local stellar orbits independent at Galaxy-
expect(session.pageErrors).toEqual([]);
});

test('served non-Galaxy spacetime controls keep their full normalized range', async ({ page }) => {
const session = await openDashboard(page);
await page.goto('/');
await page.locator('.nav-item[data-view="relations"]').click();
await expect(page.locator('#graph-canvas canvas').first()).toBeAttached({ timeout: 20_000 });
await page.locator('[data-graph-preset-choice="compact"]').click();
await page.waitForFunction(() => window.__engraphisGraph
&& window.__engraphisGraph.state().settings.mode === 'compact'
&& window.__fg);

const report = await page.evaluate(() => {
const set = (id, value) => {
const control = document.getElementById(id);
control.value = String(value);
control.dispatchEvent(new Event('input', { bubbles: true }));
};
const mass = [20, 40, 160, 460, 500].map(value => {
set('graph-black-hole-mass', value);
return { value, multiplier: window.__engraphisGraph.state().settings.blackHoleMass };
});
const damping = [1, 15].map(value => {
set('graph-space-damping', value);
return { value, decay: window.__fg.d3VelocityDecay() };
});
return { mass, damping };
});

expect(report.mass).toEqual([
{ value: 20, multiplier: 0.125 },
{ value: 40, multiplier: 0.25 },
{ value: 160, multiplier: 1 },
{ value: 460, multiplier: 4 },
{ value: 500, multiplier: 4.4 },
]);
/* The merged engine interpolates damping onto d3's velocityDecay with a size-aware
baseline: damping=0 -> 0.05 floor, damping=1 -> the neutral settling baseline
(0.38 small / 0.45 large), damping=15 -> 0.85 ceiling. This served graph is small. */
expect(report.damping[0].decay).toBeCloseTo(0.38, 12);
expect(report.damping[1].decay).toBeCloseTo(0.85, 12);
expect(session.pageErrors).toEqual([]);
});

test('Galaxy motion is 50 percent faster while core perturbation stays bound', async ({ page }) => {
await openDashboard(page, { query: '?graph-engine=next' });
await openGraphView(page);
Expand Down
13 changes: 5 additions & 8 deletions tests/test_galaxy_gravity_floor.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,9 @@ def test_gravity_slider_every_integer_changes_carrier_radius_strictly() -> None:
# Coarse contract: the loose endpoint (s=0) must be substantially looser than the
# full tight endpoint (s=400, the user's "tight"). The slider's full range is
# 0..400 (HTML min/max); 96 is the preset baseline, NOT the tight end.
# Contract: loose radius ≥ 1.5x tight radius (i.e. at least 33% contraction).
# The final painted-edge contact projection can cap the tight endpoint when this
# fixture's black-hole horizon intersects a contracted carrier. Keep a substantial
# 20% contraction contract after that invariant is enforced.
report_full = _run(
"""
const scene = """ + json.dumps(SCENE) + """;
Expand All @@ -206,15 +208,10 @@ def test_gravity_slider_every_integer_changes_carrier_radius_strictly() -> None:
)
loose = report_full["loose"]
tight = report_full["tight"]
# 1.25x calibration: the merged renderer (PR spacetime feature + main floor removal)
# produces ~1.30x loose/tight contraction across the full 0..400 travel. The floor
# removal contract is that every tick is distinct AND the full travel produces a
# substantial visible contraction; 1.25x leaves regression headroom below the
# measured 1.30x while still catching any plateau collapse (the old bug was 1.0x).
assert loose >= tight * 1.25, (
f"Slider 0 (loose) vs 400 (tight): radii {loose:.2f} vs {tight:.2f}; "
f"the slider should contract carriers by at least 20% across its full "
f"range (loose >= 1.25x tight), but the ratio is only "
f"range (loose >= 1.25x tight) after contact projection, but the ratio is only "
f"{loose / tight if tight > 0 else float('inf'):.2f}x. The combined "
f"response-mapping + floor-removal fix should let the slider's full "
f"0..400 travel produce a substantial visible contraction."
Expand Down Expand Up @@ -287,4 +284,4 @@ def test_gravity_slider_global_field_unfloored_zero_loose_endpoint() -> None:
f"tight endpoint should produce a substantially stronger central force "
f"than the loose endpoint; the renderer's prior floor plateau collapsed "
f"both ends to roughly the same force."
)
)
Loading