|
22 | 22 | const MAX_CENTROID_GROUPS = 512; |
23 | 23 |
|
24 | 24 | 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 | + }; |
26 | 30 | let generation = 0; |
27 | 31 |
|
28 | 32 | function post(message) { self.postMessage(message); } |
|
217 | 221 | springs run weak — they are visual routes between districts, not licence to drag |
218 | 222 | the districts into one another over the settle passes. */ |
219 | 223 | 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; |
220 | 229 | /* Bumped from *1.6 to *2.4 — the link-distance slider now produces 50% more spring |
221 | 230 | rest-length change per slider unit, so the upper half of the slider is meaningfully |
222 | 231 | more responsive. */ |
|
227 | 236 | const dist = Math.sqrt(ddx * ddx + ddy * ddy) || 0.0001; |
228 | 237 | const crossCommunity = |
229 | 238 | model.communities[a] !== model.communities[b] ? 0.02 : 0.07; |
230 | | - const force = (dist - rest) / dist * crossCommunity; |
231 | | - dx[a] -= ddx * force; dy[a] -= ddy * force; |
232 | | - dx[b] += ddx * force; dy[b] += ddy * force; |
| 239 | + const force = (dist - rest) / dist * crossCommunity * springScale; |
| 240 | + /* `force` is positive when the pair is beyond rest and negative when it overlaps. |
| 241 | + Apply equal-and-opposite corrections along a→b so positive force attracts the pair |
| 242 | + and negative force separates it. */ |
| 243 | + dx[a] += ddx * force; dy[a] += ddy * force; |
| 244 | + dx[b] -= ddx * force; dy[b] -= ddy * force; |
233 | 245 | } |
234 | 246 |
|
235 | 247 | /* Local repulsion through a spatial hash with a per-node visit cap keeps each pass O(n) |
|
246 | 258 | const minDist2 = minDist * minDist; |
247 | 259 | /* Bumped from /48 to /24 — the Every-node engine now produces 100% more repulsion per |
248 | 260 | slider unit, so the upper half of the repel slider is meaningfully more responsive. */ |
249 | | - const push = Number(settings.repel) / 24; |
| 261 | + /* The dashboard maps the 0..200 Cluster cohesion slider to localGravitationalConstant |
| 262 | + 0..4, so clamping at 2 left the entire upper half of the control inert: it is this |
| 263 | + worker's only consumer of the setting (PR #177 review thread at this site). Accept |
| 264 | + the full emitted range and floor the inverted push coefficient at zero so a high |
| 265 | + cohesion cannot turn the collision-style push into an attraction. */ |
| 266 | + const cohesion = Number.isFinite(Number(settings.localGravitationalConstant)) |
| 267 | + ? Math.max(0, Math.min(4, Number(settings.localGravitationalConstant))) : 1; |
| 268 | + /* Cluster cohesion strengthens the attractive spring network above. Invert its influence |
| 269 | + on the collision-style push so a higher cohesion setting does not spread clusters apart. */ |
| 270 | + const push = Number(settings.repel) / 24 * Math.max(0, 1.5 - 0.5 * cohesion); |
250 | 271 | for (let index = 0; index < count; index += 1) { |
251 | 272 | const gx = Math.floor(pos[index * 2] / cell), gy = Math.floor(pos[index * 2 + 1] / cell); |
252 | 273 | let checked = 0; |
|
320 | 341 | } |
321 | 342 |
|
322 | 343 | /* Bumped from 0.0015 to 0.0033 — combined with the base gravity 25% bump and the |
323 | | - linear (no-sqrt) mass path, the Every-node worker now pulls nodes toward the centre |
324 | | - ~50% harder at every slider position than the previous 0.0022 calibration. */ |
325 | | - const gravity = Number(settings.gravity) / 48 * 0.0033; |
| 344 | + linear (no-sqrt) mass path, the Every-node worker pulls nodes toward the centre |
| 345 | + ~50% harder at every slider position than the previous 0.0022 calibration. |
| 346 | + The dashboard emits Core attraction over 0..4 (raw/50) and Core mass up to 4.4; |
| 347 | + clamping at 2 left the upper half of those controls inert (PR #177 review thread |
| 348 | + at this site), so the full emitted ranges are consumed. */ |
| 349 | + const coreAttraction = Number.isFinite(Number(settings.gravitationalConstant)) |
| 350 | + ? Math.max(0, Math.min(4, Number(settings.gravitationalConstant))) : 1; |
| 351 | + const coreMass = Number.isFinite(Number(settings.blackHoleMass)) |
| 352 | + ? Math.max(0, Math.min(4.4, Number(settings.blackHoleMass))) : 1; |
| 353 | + const gravity = Number(settings.gravity) / 48 * 0.0033 * coreAttraction * coreMass; |
326 | 354 | for (let index = 0; index < count; index += 1) { |
327 | 355 | dx[index] += (cx - pos[index * 2]) * gravity; |
328 | 356 | dy[index] += (cy - pos[index * 2 + 1]) * gravity; |
329 | 357 | } |
330 | 358 |
|
331 | 359 | /* A tight per-pass step cap keeps the settle from smearing district boundaries. */ |
332 | | - const damp = 0.8, maxStep = SPACING * MAP_SCALE * 0.7; |
| 360 | + const resistance = Number.isFinite(Number(settings.damping)) |
| 361 | + ? Math.max(0, Math.min(15, Number(settings.damping))) : 1; |
| 362 | + /* Keep the full 0..15 control range responsive: the reciprocal curve reaches 0.2 |
| 363 | + at resistance 13, so a 0.2 floor would make the final two slider units inert. */ |
| 364 | + const damp = Math.max(0.15, Math.min(0.95, 0.8 / (0.75 + 0.25 * resistance))); |
| 365 | + const maxStep = SPACING * MAP_SCALE * 0.7; |
333 | 366 | for (let index = 0; index < count; index += 1) { |
334 | 367 | let vx = dx[index] * damp, vy = dy[index] * damp; |
335 | 368 | const speed = Math.sqrt(vx * vx + vy * vy); |
|
417 | 450 | repel: Number.isFinite(Number(next.repel)) ? Number(next.repel) : settings.repel, |
418 | 451 | link: Number.isFinite(Number(next.link)) ? Number(next.link) : settings.link, |
419 | 452 | gravity: Number.isFinite(Number(next.gravity)) ? Number(next.gravity) : settings.gravity, |
| 453 | + gravitationalConstant: Number.isFinite(Number(next.gravitationalConstant)) |
| 454 | + ? Number(next.gravitationalConstant) : settings.gravitationalConstant, |
| 455 | + blackHoleMass: Number.isFinite(Number(next.blackHoleMass)) |
| 456 | + ? Number(next.blackHoleMass) : settings.blackHoleMass, |
| 457 | + localGravitationalConstant: Number.isFinite(Number(next.localGravitationalConstant)) |
| 458 | + ? Number(next.localGravitationalConstant) : settings.localGravitationalConstant, |
| 459 | + damping: Number.isFinite(Number(next.damping)) ? Number(next.damping) : settings.damping, |
| 460 | + springStiffness: Number.isFinite(Number(next.springStiffness)) |
| 461 | + ? Number(next.springStiffness) : settings.springStiffness, |
420 | 462 | }; |
421 | 463 | if (data.relayout && model) { |
422 | 464 | generation += 1; |
|
0 commit comments