Skip to content

Commit 408af09

Browse files
merge main into ship/gravity-slider-1to1
Reconcile the slider branch with main's reviewed spacetime implementation: non-Galaxy D3 force wiring (full-range multipliers, damping->velocityDecay interpolation, mass-multiplied centering in every preset) and the tests/test_graph_engine_asset.py suite come from main; the Galaxy physics iteration (0..400 range contracts, coalesced contact-correction, zero-floor gravity) and the floor-test contraction rationale stay on this branch. Both branch-unique regression tests are preserved in the merged suite: test_black_hole_mass_reaches_centering_forces_in_every_non_galaxy_layout and test_slider_burst_reasserts_contact_invariant_when_galaxy_is_frozen. test_graph_engine_asset: 238 pass; slider/floor/every suites green; ruff, commercial manifest, and CSP asset gates pass.
2 parents 020479e + c0fd92f commit 408af09

8 files changed

Lines changed: 12558 additions & 11900 deletions

engraphis/dashboard_assets/engraphis-graph-every-worker.js

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@
2222
const MAX_CENTROID_GROUPS = 512;
2323

2424
let model = null;
25-
let settings = { repel: 48, link: 16, gravity: 48 };
25+
let settings = {
26+
repel: 48, link: 16, gravity: 48,
27+
gravitationalConstant: 1, blackHoleMass: 1, localGravitationalConstant: 1,
28+
damping: 1, springStiffness: 1,
29+
};
2630
let generation = 0;
2731

2832
function post(message) { self.postMessage(message); }
@@ -217,16 +221,24 @@
217221
springs run weak — they are visual routes between districts, not licence to drag
218222
the districts into one another over the settle passes. */
219223
const scaledSpacing = SPACING * MAP_SCALE;
224+
const spring = Number.isFinite(Number(settings.springStiffness))
225+
? Math.max(0, Math.min(100 / 32, Number(settings.springStiffness))) : 1;
226+
// springStiffness is already a normalized multiplier from the dashboard. Preserve its
227+
// zero endpoint so the Link spring control can actually disable pair attraction.
228+
const springScale = spring;
220229
const rest = Math.max(scaledSpacing * 1.9, Number(settings.link) * 1.6 * (MAP_SCALE * 0.55));
221230
for (let edge = 0; edge < model.totalLinks; edge += 1) {
222231
const a = model.sources[edge], b = model.targets[edge];
223232
const ddx = pos[b * 2] - pos[a * 2], ddy = pos[b * 2 + 1] - pos[a * 2 + 1];
224233
const dist = Math.sqrt(ddx * ddx + ddy * ddy) || 0.0001;
225234
const crossCommunity =
226235
model.communities[a] !== model.communities[b] ? 0.02 : 0.07;
227-
const force = (dist - rest) / dist * crossCommunity;
228-
dx[a] -= ddx * force; dy[a] -= ddy * force;
229-
dx[b] += ddx * force; dy[b] += ddy * force;
236+
const force = (dist - rest) / dist * crossCommunity * springScale;
237+
/* `force` is positive when the pair is beyond rest and negative when it overlaps.
238+
Apply equal-and-opposite corrections along a→b so positive force attracts the pair
239+
and negative force separates it. */
240+
dx[a] += ddx * force; dy[a] += ddy * force;
241+
dx[b] -= ddx * force; dy[b] -= ddy * force;
230242
}
231243

232244
/* Local repulsion through a spatial hash with a per-node visit cap keeps each pass O(n)
@@ -241,7 +253,16 @@
241253
}
242254
const minDist = SPACING * MAP_SCALE * 1.55;
243255
const minDist2 = minDist * minDist;
244-
const push = Number(settings.repel) / 48;
256+
/* The dashboard maps the 0..200 Cluster cohesion slider to localGravitationalConstant
257+
0..4, so clamping at 2 left the entire upper half of the control inert: it is this
258+
worker's only consumer of the setting (PR #177 review thread at this site). Accept
259+
the full emitted range and floor the inverted push coefficient at zero so a high
260+
cohesion cannot turn the collision-style push into an attraction. */
261+
const cohesion = Number.isFinite(Number(settings.localGravitationalConstant))
262+
? Math.max(0, Math.min(4, Number(settings.localGravitationalConstant))) : 1;
263+
/* Cluster cohesion strengthens the attractive spring network above. Invert its influence
264+
on the collision-style push so a higher cohesion setting does not spread clusters apart. */
265+
const push = Number(settings.repel) / 48 * Math.max(0, 1.5 - 0.5 * cohesion);
245266
for (let index = 0; index < count; index += 1) {
246267
const gx = Math.floor(pos[index * 2] / cell), gy = Math.floor(pos[index * 2 + 1] / cell);
247268
let checked = 0;
@@ -314,14 +335,26 @@
314335
}
315336
}
316337

317-
const gravity = Number(settings.gravity) / 48 * 0.0015;
338+
/* The dashboard emits Core attraction/local cohesion over 0..4 (raw/50) and Core mass
339+
up to 4.4; clamping at 2 left the upper half of those controls inert (PR #177 review
340+
thread at this site). Accept the full emitted ranges. */
341+
const coreAttraction = Number.isFinite(Number(settings.gravitationalConstant))
342+
? Math.max(0, Math.min(4, Number(settings.gravitationalConstant))) : 1;
343+
const coreMass = Number.isFinite(Number(settings.blackHoleMass))
344+
? Math.max(0, Math.min(4.4, Number(settings.blackHoleMass))) : 1;
345+
const gravity = Number(settings.gravity) / 48 * 0.0015 * coreAttraction * coreMass;
318346
for (let index = 0; index < count; index += 1) {
319347
dx[index] += (cx - pos[index * 2]) * gravity;
320348
dy[index] += (cy - pos[index * 2 + 1]) * gravity;
321349
}
322350

323351
/* A tight per-pass step cap keeps the settle from smearing district boundaries. */
324-
const damp = 0.8, maxStep = SPACING * MAP_SCALE * 0.7;
352+
const resistance = Number.isFinite(Number(settings.damping))
353+
? Math.max(0, Math.min(15, Number(settings.damping))) : 1;
354+
/* Keep the full 0..15 control range responsive: the reciprocal curve reaches 0.2
355+
at resistance 13, so a 0.2 floor would make the final two slider units inert. */
356+
const damp = Math.max(0.15, Math.min(0.95, 0.8 / (0.75 + 0.25 * resistance)));
357+
const maxStep = SPACING * MAP_SCALE * 0.7;
325358
for (let index = 0; index < count; index += 1) {
326359
let vx = dx[index] * damp, vy = dy[index] * damp;
327360
const speed = Math.sqrt(vx * vx + vy * vy);
@@ -409,6 +442,15 @@
409442
repel: Number.isFinite(Number(next.repel)) ? Number(next.repel) : settings.repel,
410443
link: Number.isFinite(Number(next.link)) ? Number(next.link) : settings.link,
411444
gravity: Number.isFinite(Number(next.gravity)) ? Number(next.gravity) : settings.gravity,
445+
gravitationalConstant: Number.isFinite(Number(next.gravitationalConstant))
446+
? Number(next.gravitationalConstant) : settings.gravitationalConstant,
447+
blackHoleMass: Number.isFinite(Number(next.blackHoleMass))
448+
? Number(next.blackHoleMass) : settings.blackHoleMass,
449+
localGravitationalConstant: Number.isFinite(Number(next.localGravitationalConstant))
450+
? Number(next.localGravitationalConstant) : settings.localGravitationalConstant,
451+
damping: Number.isFinite(Number(next.damping)) ? Number(next.damping) : settings.damping,
452+
springStiffness: Number.isFinite(Number(next.springStiffness))
453+
? Number(next.springStiffness) : settings.springStiffness,
412454
};
413455
if (data.relayout && model) {
414456
generation += 1;

engraphis/dashboard_assets/engraphis-graph-every.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
(function () {
99
'use strict';
1010

11-
const WORKER_URL = '/v2-assets/engraphis-graph-every-worker.js?v=20260823-every-19';
11+
const WORKER_URL = '/v2-assets/engraphis-graph-every-worker.js?v=20260830-spacetime-controls-20';
1212
const MAX_NODES = 20000;
1313
const MAX_LINKS = 200000;
1414
const LABEL_MAX = 220;
@@ -151,7 +151,9 @@
151151
totalLinks: 0, edgeVertexCount: 0,
152152
camera: { x: 0, y: 0, scale: 1 }, baseScale: 1, width: 1, height: 1, dpr: 1,
153153
styleName: opts.style || 'cyber', colorBy: 'community', typeColors: {}, themeColors: {}, palette: 'theme',
154-
settings: { labels: true, flow: false, flowSpeed: 45, frozen: false, mode: 'communities', repel: 48, link: 16, gravity: 48, font: 12, size: 3, linkw: 0.72 },
154+
settings: { labels: true, flow: false, flowSpeed: 45, frozen: false, mode: 'communities', repel: 48, link: 16, gravity: 48, font: 12, size: 3, linkw: 0.72,
155+
gravitationalConstant: 1, blackHoleMass: 1, localGravitationalConstant: 1,
156+
damping: 1, springStiffness: 1 },
155157
sizeBy: 'degree', bridges: true, ghosts: true,
156158
scope: { minDegree: 0, showUnlinked: true, depth: 2 },
157159
collapse: false, collapsed: false,
@@ -1376,7 +1378,10 @@
13761378
const patch = value || {};
13771379
state.settings = { ...state.settings, ...patch };
13781380
state.flowPaintAt = 0;
1379-
const relayout = Object.keys(patch).some(key => ['mode', 'repel', 'link', 'gravity'].includes(key));
1381+
const relayout = Object.keys(patch).some(key => [
1382+
'mode', 'repel', 'link', 'gravity', 'gravitationalConstant', 'blackHoleMass',
1383+
'localGravitationalConstant', 'damping', 'springStiffness',
1384+
].includes(key));
13801385
postSettings(relayout);
13811386
uploadNodeMeta();
13821387
camera();

0 commit comments

Comments
 (0)