Module

Hylograph.Simulation.Core.Engine

Package
purescript-hylograph-simulation-core
Repository
afcondon/purescript-hylograph-simulation-core

Scene Engine

Tick-based scene orchestration with adapter pattern.

The engine manages transitions between scenes using a three-phase lifecycle:

  1. Initialize: Apply init rules to prepare starting state
  2. Transition: Interpolate positions from start to target
  3. Finalize: Apply final rules and enter stable mode

The engine is generic over the node type. Apps provide an adapter record with functions that interact with their specific simulation kernel.

Example usage:

-- Create adapter for your kernel (D3, WASM, etc.)
adapter = mkAdapter mySimulation

-- Create engine
engineRef <- createEngine adapter

-- Transition to a scene
transitionTo treeFormScene engineRef

-- Call tick from your simulation's tick handler
Sim.onTick (\_ -> tick engineRef) mySimulation

#SceneEngine Source

type SceneEngine node = Ref (EngineState node)

The scene engine, stored in a Ref for mutation across tick callbacks

#EngineState Source

type EngineState node = { adapter :: EngineAdapter node, currentScene :: Maybe (SceneConfig node), transition :: Maybe (TransitionState node), transitionDelta :: TickDelta }

Internal engine state

#EngineAdapter Source

type EngineAdapter node = { applyRulesInPlace :: Array (NodeRule node) -> Effect Unit, capturePositions :: Array node -> PositionMap, getNodes :: Effect (Array node), interpolatePositions :: PositionMap -> PositionMap -> Number -> Effect Unit, reheat :: Effect Unit, reinitializeForces :: Effect Unit, updatePositions :: PositionMap -> Effect Unit }

Adapter functions that connect the engine to your simulation kernel.

The engine is generic - it doesn't know about D3 or WASM directly. Instead, you provide these adapter functions that do the actual work.

Each kernel (D3, WASM, etc.) implements mkAdapter to create this record.

Example adapter for D3:

mkAdapter :: D3Simulation -> EngineAdapter SimNode
mkAdapter sim =
  { getNodes: Sim.getNodes sim
  , capturePositions: \nodes -> Object.fromFoldable $
      map (\n -> Tuple (show n.id) { x: n.x, y: n.y }) nodes
  , interpolatePositions: \start target progress ->
      Sim.interpolatePositionsInPlace start target progress sim
  , updatePositions: \positions ->
      Sim.updatePositionsInPlace positions sim
  , applyRulesInPlace: \rules ->
      applyRulesInPlace_ rules sim.nodes
  , reinitializeForces: reinitForcesFor sim
  , reheat: Sim.reheat sim
  }

#createEngine Source

createEngine :: forall node. EngineAdapter node -> Effect (SceneEngine node)

Create a new scene engine.

The engine starts with no active scene. Call transitionTo to begin your first scene transition.

Uses default transition duration of ~2 seconds.

#transitionTo Source

transitionTo :: forall node. SceneConfig node -> SceneEngine node -> Effect Unit

Start a transition to a new scene.

If a transition is already in progress, this is ignored.

The transition lifecycle:

  1. Apply scene's initRules to prepare starting state
  2. Capture current positions as start positions
  3. Calculate target positions via scene's layout function
  4. Begin interpolation (handled by tick)

#tick Source

tick :: forall node. SceneEngine node -> Effect Boolean

Advance the engine by one tick.

Call this from your simulation's tick handler.

Returns true if a transition is in progress, false if stable.

#getCurrentScene Source

getCurrentScene :: forall node. SceneEngine node -> Effect (Maybe (SceneConfig node))

Get the current scene configuration (if any).

#isTransitioning Source

isTransitioning :: forall node. SceneEngine node -> Effect Boolean

Check if a transition is in progress.

#getTransitionProgress Source

getTransitionProgress :: forall node. SceneEngine node -> Effect (Maybe Number)

Get the current transition progress (0.0 to 1.0).

Returns Nothing if no transition is in progress.

#setCurrentScene Source

setCurrentScene :: forall node. SceneConfig node -> SceneEngine node -> Effect Unit

Set the current scene directly (without transition).

Use this when the visualization is already in the target state (e.g., after initial rendering with pre-computed positions).

#defaultTransitionDelta Source

defaultTransitionDelta :: TickDelta

Default transition delta: ~2 seconds at 60fps

Re-exports from Hylograph.Simulation.Core.Types

#TransitionState Source

type TransitionState node = { progress :: Number, startPositions :: PositionMap, targetPositions :: PositionMap, targetScene :: SceneConfig node }

Runtime state during an active transition.

Tracks the target scene, start/end positions, and current progress. The interpolation engine updates progress each tick until complete. Uses tick-based progress (0.0 to 1.0) rather than time-based elapsed.

#SceneConfig Source

type SceneConfig node = { finalRules :: Array node -> Array (NodeRule node), initRules :: Array (NodeRule node), layout :: Array node -> PositionMap, name :: String, stableMode :: EngineMode }

Scene configuration with three-phase lifecycle.

Phase 1: Initialize (initRules) Applied before transition starts. Use this to set up starting positions, e.g., moving tree nodes to the root for a "grow from root" animation.

Phase 2: Transition (layout) The interpolation engine smoothly moves nodes from their current positions to the target positions computed by the layout function.

Phase 3: Finalize (finalRules) Applied after transition completes. Use this to set up the stable state, e.g., unpinning nodes so forces can take over, or setting gridX/gridY.

Example:

treeFormScene :: SceneConfig MyNode
treeFormScene =
  { name: "TreeForm"
  , initRules: [ moveToRootRule ]
  , layout: \nodes -> computeTreePositions nodes
  , finalRules: \_ -> [ pinAtTreePositionsRule ]
  , stableMode: Static
  }

#PositionMap Source

type PositionMap = Object Position

Position map: node ID (as string) -> position Used for capturing current positions and specifying targets

#NodeRule Source

type NodeRule node = { apply :: node -> node, name :: String, select :: node -> Boolean }

A rule that selects nodes and applies a transform.

Rules are applied with first-match-wins semantics (like CSS cascade). If multiple rules match a node, only the first one applies.

Example:

pinPackages :: NodeRule MyNode
pinPackages =
  { name: "pinPackages"
  , select: \n -> n.nodeType == Package
  , apply: \n -> n { fx = notNull n.x, fy = notNull n.y }
  }

#EngineMode Source

data EngineMode

Engine mode determines what happens after a transition completes.

  • Physics: Force simulation runs, nodes settle via forces
  • Static: Nodes stay pinned at their final positions

Constructors

Instances