Files
app/frontend/src/components/Flow/brainLayout.worker.ts
T

55 lines
1.7 KiB
TypeScript

/**
* The brain's force layout, off the render thread.
*
* Run to rest in one go, it took several frames on the main thread: about
* 75 ms on a page's first layout and 30 ms after for the Demo's 29 neurons,
* and 250 ms at five times that. Answers with where each neuron settled, in
* the order they came.
*
* `forceX`/`forceY` rather than `forceCenter`: centering only translates the
* whole thing, so unconnected flows — which is most of them — would push each
* other apart forever with nothing pulling back.
*/
import {
forceCollide,
forceLink,
forceManyBody,
forceSimulation,
forceX,
forceY,
type SimulationNodeDatum,
} from "d3-force"
export type Seed = SimulationNodeDatum & { id: string; room: number }
export type LayoutRequest = {
placed: Seed[]
links: { source: string; target: string }[]
}
self.onmessage = ({ data }: MessageEvent<LayoutRequest>) => {
const { placed, links } = data
forceSimulation(placed)
.force(
"link",
forceLink(links)
.id((node) => (node as Seed).id)
.distance(130),
)
.force("charge", forceManyBody().strength(-320).distanceMax(500))
.force(
"collide",
forceCollide<Seed>((node) => node.room),
)
// Centring pulls nearly three times as hard vertically as horizontally, so
// the graph compresses into a band roughly twice as wide as it is tall
// instead of the square an even pull settles into. Coming out of the
// simulation rather than out of a transform keeps the circles round and the
// edges the length the link force asked for.
.force("x", forceX(0).strength(0.05))
.force("y", forceY(0).strength(0.14))
.stop()
.tick(300)
self.postMessage(placed.map((node) => [node.x ?? 0, node.y ?? 0]))
}