Hylograph.ForceEngine
- Package
- purescript-hylograph-simulation
- Repository
- afcondon/purescript-hylograph-simulation
Pure Force Engine
A clean, debuggable force simulation engine. Uses D3's force calculation algorithms but manages the simulation ourselves.
Key benefits over d3.forceSimulation:
- Full control over when forces run
- Predictable, debuggable behavior
- Clean PureScript types
- No hidden state or timers
Usage:
import Hylograph.ForceEngine as FE
main = do
sim <- FE.create FE.defaultConfig
FE.setNodes myNodes sim
FE.addForce (FE.ManyBody "charge" FE.defaultManyBody) sim
FE.onTick (renderNodes sim) sim
FE.start sim
Re-exports from Hylograph.ForceEngine.Core
#ForceHandle Source
data ForceHandle#initializeNodes Source
initializeNodes :: forall r. Array (Record r) -> Effect UnitInitialize nodes with indices and default velocities This mutates the nodes array to add index, vx, vy if missing
#initializeLinkForce Source
initializeLinkForce :: forall nodeRow linkRow. ForceHandle -> Array (Record nodeRow) -> Array (Record linkRow) -> Effect ForceHandleInitialize a link force with nodes and links Link forces need both nodes and links
#initializeForce Source
initializeForce :: forall r. ForceHandle -> Array (Record r) -> Effect ForceHandleInitialize a force with nodes Must be called before applying the force
#createRadial Source
createRadial :: RadialConfig -> ForceHandleCreate a radial force
#createManyBody Source
createManyBody :: ManyBodyConfig -> ForceHandleCreate a many-body (charge) force
#createLink Source
createLink :: LinkConfig -> ForceHandleCreate a link force
#createForceY Source
createForceY :: ForceYConfig -> ForceHandleCreate a Y positioning force
#createForceX Source
createForceX :: ForceXConfig -> ForceHandleCreate an X positioning force
#createCollide Source
createCollide :: CollideConfig -> ForceHandleCreate a collision force
#createCenter Source
createCenter :: CenterConfig -> ForceHandleCreate a centering force
#applyForces Source
applyForces :: Array ForceHandle -> Number -> Effect UnitApply multiple forces in sequence
#applyForce Source
applyForce :: ForceHandle -> Number -> Effect UnitApply a single force This mutates vx/vy on the nodes the force was initialized with
Re-exports from Hylograph.ForceEngine.Events
#SimulationEvent Source
data SimulationEventEvents that can occur during simulation lifecycle.
These types are primarily for Halogen integration via subscriptions. Internally, callbacks are invoked directly without constructing these values.
Usage with Halogen:
handleAction (SimEvent event) = case event of
Tick -> liftEffect updateDOMPositions
Started -> H.modify_ _ { simRunning = true }
Stopped -> H.modify_ _ { simRunning = false }
AlphaDecayed alpha -> when (alpha < 0.1) $ liftEffect doSomething
Constructors
Instances
#SimulationCallbacks Source
type SimulationCallbacks = { onAlphaThreshold :: Ref (Number -> Effect Unit), onStart :: Ref (Effect Unit), onStop :: Ref (Effect Unit), onTick :: Ref (Effect Unit) }Callbacks for simulation events.
All callbacks are stored in Refs to allow dynamic updates and to keep the tick loop allocation-free (no closures created per-tick).
The callbacks are Effect Unit rather than taking event data because:
- Performance: No boxing/unboxing of event data in hot path
- Simplicity: Consumers can read simulation state directly if needed
- Flexibility: The consumer decides what data they need
#onSimulationTick Source
onSimulationTick :: Effect Unit -> SimulationCallbacks -> Effect UnitSet the tick callback.
WARNING: This callback runs 60x/second during simulation. Keep it fast! Avoid allocations, complex logic, or console.log.
Good:
onSimulationTick (updateGroupPositions nodes) callbacks
Bad:
onSimulationTick (do
nodes <- Ref.read nodesRef -- Extra ref read
log "tick" -- Console I/O
H.tell ... -- Halogen message (allocates)
) callbacks
#onSimulationStop Source
onSimulationStop :: Effect Unit -> SimulationCallbacks -> Effect UnitSet the stop callback.
Called when simulation stops (alpha < alphaMin). Safe to do heavier work here.
#onSimulationStart Source
onSimulationStart :: Effect Unit -> SimulationCallbacks -> Effect UnitSet the start callback.
Called when simulation starts running or is reheated from stopped state. Safe to do heavier work here (only called occasionally).
#onAlphaDecay Source
onAlphaDecay :: (Number -> Effect Unit) -> SimulationCallbacks -> Effect UnitSet the alpha threshold callback.
Called when alpha crosses common thresholds: 0.5, 0.1, 0.01. The callback receives the new alpha value.
Useful for:
- Progressive rendering (show more detail as simulation settles)
- UI updates (e.g., hide "Simulating..." spinner)
- Analytics (track how long simulations run)
#defaultCallbacks Source
defaultCallbacks :: Effect SimulationCallbacksCreate default callbacks (all no-ops).
Usage:
callbacks <- defaultCallbacks
Ref.write updateDOMPositions callbacks.onTick
sim <- createWithCallbacks config callbacks
Re-exports from Hylograph.ForceEngine.Links
#swizzleLinksByIndex Source
swizzleLinksByIndex :: forall node rawLink swizzled. (node -> Int) -> Array node -> Array { source :: Int, target :: Int | rawLink } -> (node -> node -> Int -> { source :: Int, target :: Int | rawLink } -> swizzled) -> Array swizzledSwizzle links by looking up nodes by their index field
USE THIS when working with a FILTERED node subset where array positions
don't match node.index values. Looks up nodes by their .index field
and silently drops links where either endpoint is not found.
PERFORMANCE: Uses FFI-backed IntMap for O(1) lookups instead of O(n) find.
This is essential for GUP patterns where you show only a subset of nodes.
Example:
let visibleNodes = filter isVisible allNodes
swizzled = swizzleLinksByIndex _.index visibleNodes links \src tgt i link ->
{ source: src, target: tgt, index: i, value: link.value }
#swizzleLinks Source
swizzleLinks :: forall node rawLink swizzled. Array node -> Array { source :: Int, target :: Int | rawLink } -> (node -> node -> Int -> { source :: Int, target :: Int | rawLink } -> swizzled) -> Array swizzledConvert raw links (integer indices) to swizzled links (node references)
Expects link.source and link.target to be valid array indices into the
nodes array. Links with out-of-bounds indices are silently dropped.
Note: If your link indices are semantic (node.index field values) rather than
array positions, use swizzleLinksByIndex instead, which looks up nodes by
their .index field.
The transform function allows you to build the output link record, copying extra fields from the raw link as needed.
Example:
let swizzled = swizzleLinks nodes rawLinks \src tgt i link ->
{ source: src, target: tgt, index: i, value: link.value }
#filterLinksToSubset Source
filterLinksToSubset :: forall node linkData. (node -> Int) -> Array node -> Array { source :: Int, target :: Int | linkData } -> Array { source :: Int, target :: Int | linkData }Filter links to only those where both endpoints are in the node subset
PERFORMANCE: Uses FFI-backed IntSet for O(1) membership instead of O(n) elem.
Use this before swizzling when you want to show links only between
visible/selected nodes. Pairs naturally with swizzleLinksByIndex.
Example:
let visibleNodes = filter isVisible allNodes
visibleLinks = filterLinksToSubset _.index visibleNodes allLinks
swizzled = swizzleLinksByIndex _.index visibleNodes visibleLinks transform
Re-exports from Hylograph.ForceEngine.Registry
#AnySimulation Source
data AnySimulationInternal registry storage. Uses existential encoding to store simulations with different row types. The trade-off is we can only perform operations that work on any simulation.
#unsafeFromAny Source
unsafeFromAny :: forall row linkRow. AnySimulation -> Simulation row linkRow#unregister Source
unregister :: String -> Effect UnitUnregister a simulation by name.
Does nothing if no simulation with this name exists. The simulation is NOT stopped automatically.
#register Source
register :: forall row linkRow. String -> Simulation row linkRow -> Effect UnitRegister a simulation with a name.
If a simulation with this name already exists, it will be replaced. The old simulation is NOT stopped automatically.
sim <- create defaultConfig
register "call-graph" sim
#lookup Source
lookup :: String -> Effect (Maybe AnySimulation)Look up a simulation by name.
WARNING: The returned simulation has an unknown row type. Only use operations that work on any simulation (stop, isRunning, getAlpha). Calling getNodes will require an unsafe cast.
#listSimulations Source
listSimulations :: Effect (Array String)List all registered simulation names.
#debugRegistry Source
debugRegistry :: Effect UnitPrint debug information about all registered simulations.
Designed for use from browser console:
// In browser console:
import('./output/Hylograph.ForceEngine.Registry/index.js').then(m => m.debugRegistry())
#clearRegistry Source
clearRegistry :: Effect UnitClear the registry without stopping simulations.
Use stopAll first if you want to stop them.
Re-exports from Hylograph.ForceEngine.Simulation
#Simulation Source
type Simulation :: Row Type -> Row Type -> Typetype Simulation row linkRow = { alpha :: Ref Number, callbacks :: Maybe SimulationCallbacks, cancelAnimation :: Ref (Effect Unit), config :: SimConfig, forces :: Ref (Map String ForceHandle), links :: Ref (Array { source :: Int, target :: Int | linkRow }), nodes :: Ref (Array (SimulationNode row)), prevAlpha :: Ref Number, running :: Ref Boolean, tickCallback :: Ref (Effect Unit) }
A running simulation This is a mutable structure that holds the simulation state
#tick Source
tick :: forall row linkRow. Simulation row linkRow -> Effect NumberRun a single tick of the simulation Returns the new alpha value
#start Source
start :: forall row linkRow. Simulation row linkRow -> Effect UnitStart the simulation animation loop
#setNodes Source
setNodes :: forall row linkRow. Array (SimulationNode row) -> Simulation row linkRow -> Effect UnitSet the nodes for the simulation This initializes nodes and re-initializes all forces
#setCallbacks Source
setCallbacks :: forall row linkRow. SimulationCallbacks -> Simulation row linkRow -> Simulation row linkRowSet the callbacks for a simulation (new multi-callback API) Note: This replaces any callbacks passed to createWithCallbacks
#removeForce Source
removeForce :: forall row linkRow. String -> Simulation row linkRow -> Effect UnitRemove a force from the simulation
#reheat Source
reheat :: forall row linkRow. Simulation row linkRow -> Effect UnitReheat the simulation (set alpha to 1)
#isRunning Source
isRunning :: forall row linkRow. Simulation row linkRow -> Effect BooleanCheck if the simulation is running
#getNodes Source
getNodes :: forall row linkRow. Simulation row linkRow -> Effect (Array (SimulationNode row))Get the current nodes (with updated positions)
#getCallbacks Source
getCallbacks :: forall row linkRow. Simulation row linkRow -> Maybe SimulationCallbacksGet the current callbacks (if any)
#getAlpha Source
getAlpha :: forall row linkRow. Simulation row linkRow -> Effect NumberGet the current alpha value
#defaultConfig Source
defaultConfig :: SimConfigDefault simulation configuration
#createWithCallbacks Source
createWithCallbacks :: forall row linkRow. SimConfig -> SimulationCallbacks -> Effect (Simulation row linkRow)Create a new simulation with callback system
Example:
callbacks <- defaultCallbacks
onSimulationTick updateDOM callbacks
onSimulationStop (log "Simulation settled") callbacks
sim <- createWithCallbacks config callbacks
#create Source
create :: forall row linkRow. SimConfig -> Effect (Simulation row linkRow)Create a new simulation (without callbacks)
For the new callback-based API, use createWithCallbacks instead.
#attachGroupDrag Source
attachGroupDrag :: forall row linkRow. Array Element -> String -> Simulation row linkRow -> Effect UnitAttach drag to transformed group elements (like bubble packs)
Unlike attachDrag, this version takes a container selector to
get pointer coordinates in the correct coordinate space. Use this when
dragging <g> elements that have transform attributes.
Example:
-- For bubble packs inside a zoom group
packElements <- select ".module-pack" >>= selectAll
attachGroupDrag packElements "#zoom-group" sim
#attachDrag Source
attachDrag :: forall row linkRow. Array Element -> Simulation row linkRow -> Effect UnitAttach simulation-aware drag behavior to node elements
This sets up D3 drag handlers that:
- Reheat the simulation on drag start
- Update fx/fy (fixed position) during drag
- Clear fx/fy on drag end (release node)
Example:
elements <- select ".node" >>= selectAll
attachDrag elements sim
#addForce Source
addForce :: forall row linkRow. ForceSpec -> Simulation row linkRow -> Effect UnitAdd a force to the simulation
Re-exports from Hylograph.ForceEngine.Types
#SimulationState Source
type SimulationState :: Row Type -> Row Type -> Typetype SimulationState nodeRow linkData = { alpha :: Number, alphaDecay :: Number, alphaMin :: Number, alphaTarget :: Number, links :: Array (RawLink linkData), nodes :: Array (SimNode nodeRow), running :: Boolean, velocityDecay :: Number }
The complete state of a force simulation. We manage this ourselves instead of letting D3 do it.
#SimNode Source
type SimNode :: Row Type -> Typetype SimNode extra = { index :: Int, vx :: Number, vy :: Number, x :: Number, y :: Number | extra }
A simulation node with position and velocity. This is the minimal structure that D3 force functions expect.
The extra row allows extending with application-specific data.
Note: vx, vy, index are added by the FFI initialization function.
#SimLink Source
type SimLink :: Row Type -> Row Type -> Typetype SimLink nodeRow linkData = { index :: Int, source :: { x :: Number, y :: Number | nodeRow }, target :: { x :: Number, y :: Number | nodeRow } | linkData }
A link between nodes. Before simulation: source/target are indices or IDs After swizzling: source/target are node references
#RadialConfig Source
type RadialConfig = { radius :: Number, strength :: Number, x :: Number, y :: Number }Configuration for radial force
#ManyBodyConfig Source
type ManyBodyConfig = { distanceMax :: Number, distanceMin :: Number, strength :: Number, theta :: Number }Configuration for a many-body (charge) force
#LinkConfig Source
type LinkConfig = { distance :: Number, iterations :: Int, strength :: Number }Configuration for a link force
#ForceYConfig Source
type ForceYConfig = { strength :: Number, y :: Number }Configuration for Y positioning force
#ForceXConfig Source
type ForceXConfig = { strength :: Number, x :: Number }Configuration for X positioning force
#CollideConfig Source
type CollideConfig = { iterations :: Int, radius :: Number, strength :: Number }Configuration for a collision force
#CenterConfig Source
type CenterConfig = { strength :: Number, x :: Number, y :: Number }Configuration for centering force
- Modules
- Hylograph.
Config. Apply - Hylograph.
Config. Force - Hylograph.
Config. Scene - Hylograph.
ForceEngine - Hylograph.
ForceEngine. Core - Hylograph.
ForceEngine. Demo - Hylograph.
ForceEngine. Events - Hylograph.
ForceEngine. Links - Hylograph.
ForceEngine. Registry - Hylograph.
ForceEngine. Render - Hylograph.
ForceEngine. Setup - Hylograph.
ForceEngine. Setup. WASM - Hylograph.
ForceEngine. Simulation - Hylograph.
ForceEngine. Types - Hylograph.
ForceEngine. WASM - Hylograph.
ForceEngine. WASMEngine - Hylograph.
Scene. Engine - Hylograph.
Scene. Handle - Hylograph.
Scene. Rules - Hylograph.
Scene. Types - Hylograph.
Simulation - Hylograph.
Simulation. Emitter - Hylograph.
Simulation. HATS - Hylograph.
Simulation. Scene - Hylograph.
Transition. Consumers - Hylograph.
Transition. Example