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:
- Initialize: Apply init rules to prepare starting state
- Transition: Interpolate positions from start to target
- 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 UnitStart a transition to a new scene.
If a transition is already in progress, this is ignored.
The transition lifecycle:
- Apply scene's
initRulesto prepare starting state - Capture current positions as start positions
- Calculate target positions via scene's
layoutfunction - Begin interpolation (handled by
tick)
#tick Source
tick :: forall node. SceneEngine node -> Effect BooleanAdvance 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 BooleanCheck 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 UnitSet 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 :: TickDeltaDefault 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 PositionPosition 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 EngineModeEngine mode determines what happens after a transition completes.
Physics: Force simulation runs, nodes settle via forcesStatic: Nodes stay pinned at their final positions