diff --git a/libs/@hashintel/petrinaut/docs/README.md b/libs/@hashintel/petrinaut/docs/README.md
new file mode 100644
index 00000000000..a7295f4b46b
--- /dev/null
+++ b/libs/@hashintel/petrinaut/docs/README.md
@@ -0,0 +1,20 @@
+# Petrinaut User Guide
+
+Petrinaut is a visual editor for [Petri nets](https://en.wikipedia.org/wiki/Petri_net).
+
+It lets you build, configure, and simulate Petri nets. It has support for various extensions including typed tokens (colours), continuous dynamics, and stochastic transitions.
+
+## Live site
+
+Petrinaut is available at [demo.petrinaut.org](https://demo.petrinaut.org).
+
+Net data will be stored in local browser storage. You can also export and import nets as JSON files for transfer between devices and browsers.
+
+## Contents
+
+- [Drawing a Net](drawing-a-net.md) -- Add nodes (places and transitions), connect them with arcs, and navigate the editor.
+- [Petri Net Extensions](petri-net-extensions.md) -- Add types, dynamics, transition kernels, firing rules, and inhibitor arcs, as well as parameters and state visualizers.
+- [Useful Patterns](useful-patterns.md) -- Common modelling techniques, including duration and resource pools.
+- [Simulation](simulation.md) -- Set initial state, run the simulation, use the timeline, and control playback.
+- [Visual Settings](visual-settings.md) -- Configure the editor appearance and behavior.
+- [Examples](examples.md) -- Walkthrough of the built-in example nets.
diff --git a/libs/@hashintel/petrinaut/docs/drawing-a-net.md b/libs/@hashintel/petrinaut/docs/drawing-a-net.md
new file mode 100644
index 00000000000..547e6377d7a
--- /dev/null
+++ b/libs/@hashintel/petrinaut/docs/drawing-a-net.md
@@ -0,0 +1,122 @@
+# Drawing a Net
+
+## Editor layout
+
+The editor is organized around a central canvas where you build your net:
+
+- **Canvas** (center) -- the main workspace where places and transitions are displayed and connected.
+- **Left sidebar** -- lists of entities organized into tabs: Nodes, Types, Differential Equations, Parameters.
+- **Properties panel** (right) -- opens when you select an entity, showing its configurable properties.
+- **Bottom panel** -- tabs for Diagnostics (code errors), Simulation Settings, and Timeline (during simulation).
+- **Bottom toolbar** -- editing mode buttons and simulation controls (+ show/hide toggle for bottom panel).
+
+
+
+## Adding places and transitions
+
+Use the bottom toolbar to add nodes:
+
+- **Add Place** (shortcut: **N**) -- click the canvas to drop a place, or click and drag the button onto the canvas.
+- **Add Transition** (shortcut: **T**) -- click the canvas to drop a transition, or drag the button onto the canvas.
+
+New nodes are named automatically (Place1, Place2, Transition1, etc.). Rename them by selecting the node and editing the name in the properties panel.
+
+
+
+## Connecting with arcs
+
+Drag from a node's handle to connect it:
+
+- **Place to Transition** creates an **input arc** (the transition consumes tokens from the place).
+- **Transition to Place** creates an **output arc** (the transition produces tokens in the place).
+
+Petri nets are bipartite: you cannot connect a place to another place or a transition to another transition. New arcs default to weight 1.
+
+
+
+## Arc weight
+
+Select an arc to open its properties. Set the **weight** to control how many tokens are consumed (input) or produced (output) per firing.
+
+You can also edit an arc's weight via the properties panel for the transition it is connected to.
+
+See also: [arc weight for multi-token operations](useful-patterns.md#arc-weight-for-multi-token-operations).
+
+## Pan and Select modes
+
+The editor has two cursor modes, toggled from the bottom toolbar dropdown:
+
+| Mode | Shortcut | Behavior |
+| ---------- | -------- | ------------------------------------------------------ |
+| **Pan** | H | Click and drag to pan the canvas. This is the default. |
+| **Select** | V | Click and drag to draw a selection box around nodes. |
+
+With a selection, you can:
+
+- **Move** -- drag selected nodes to reposition them.
+- **Delete** -- press **Backspace** or **Delete**.
+- **Copy** -- **Cmd+C** (Mac) / **Ctrl+C** (Windows/Linux).
+- **Paste** -- **Cmd+V** / **Ctrl+V**.
+
+Whether a node must be fully inside or only partially inside the selection box is configurable in [visual settings](visual-settings.md).
+
+
+
+## Left sidebar
+
+The left sidebar has four tabs for creating and managing entities:
+
+| Tab | Contents |
+| -------------------------- | --------------------------------------------------------------------- |
+| **Nodes** | All places and transitions. Click to select and open properties. |
+| **Types** | Token types (colours). Click **+** to create a new type. |
+| **Differential Equations** | ODE definitions for continuous dynamics. Click **+** to create. |
+| **Parameters** | Global parameters available in all user code. Click **+** to create. |
+
+Toggle the sidebar with the button in the top-left corner.
+
+## Search
+
+Press **Cmd+F** / **Ctrl+F** to open a search bar. Type to filter entities by name. Press **Escape** to close.
+
+## Undo / Redo
+
+Use the **Cmd+Z** / **Ctrl+Z** shortcut to undo the last action. Use the **Cmd+Shift+Z** / **Ctrl+Shift+Z** shortcut to redo the last action.
+
+The recent history is displayed in the top-right corner. Click on a history entry to go back to that state.
+
+## Keyboard shortcuts
+
+| Shortcut | Action |
+| -------------------- | --------------------------------------- |
+| N | Add Place mode |
+| T | Add Transition mode |
+| H | Pan mode |
+| V | Select mode |
+| Escape | Clear selection, return to cursor mode |
+| Cmd+A | Select all places and transitions |
+| Cmd+C | Copy selection |
+| Cmd+V | Paste |
+| Cmd+Z | Undo |
+| Cmd+Shift+Z | Redo |
+| Cmd+F | Search |
+| Delete / Backspace | Delete selection |
+
+On Windows/Linux, use Ctrl instead of Cmd.
+
+## Snap to grid
+
+When enabled, node positions snap to a grid when placing or dragging. Toggle this in [visual settings](visual-settings.md).
+
+## Import and export
+
+From the hamburger menu (top-left):
+
+- **Export as JSON** -- saves the full net definition including positions and visual styling.
+- **Export as JSON without visual info** -- strips node positions and type display colours. Useful for sharing the logical structure only.
+- **Export as TikZ** -- generates a `.tex` file with a structural diagram. This is a simplified view: no colours, inhibitor arcs, dynamics, or token types are encoded. Intended for papers and presentations.
+- **Import from JSON** -- loads a net from a `.json` file. If node positions are missing, an automatic layout is applied.
+
+## Auto-layout
+
+From the hamburger menu, select **Layout** to apply an automatic graph layout (ELK) that rearranges all nodes. Useful after importing a net without positions or when a net has become cluttered. This will not always be an improvement!
diff --git a/libs/@hashintel/petrinaut/docs/examples.md b/libs/@hashintel/petrinaut/docs/examples.md
new file mode 100644
index 00000000000..e70d1fcc745
--- /dev/null
+++ b/libs/@hashintel/petrinaut/docs/examples.md
@@ -0,0 +1,118 @@
+# Examples
+
+Petrinaut includes several built-in example nets accessible from the **hamburger menu** (top-left) under **Load example**. They are listed below from simplest to most complex.
+
+## SIR Epidemic Model
+
+The classic Susceptible-Infected-Recovered compartmental model from epidemiology, implemented as a stochastic Petri net.
+
+**Demonstrates:**
+
+- **Stochastic firing rates** controlled by [global parameters](petri-net-extensions.md#global-parameters) (`infection_rate`, `recovery_rate`).
+- **Arc weight > 1** -- the Infection transition consumes 1 Susceptible + 1 Infected and produces 2 Infected tokens, modelling the S+I -> 2I mass-action dynamics.
+- Simple parameter-driven lambdas: `parameters.infection_rate` and `parameters.recovery_rate`.
+
+**Suggested initial state:** set Susceptible to **100** tokens, Infected to **1**, Recovered to **0**. All places are untyped, so you just set token counts. Press Play and watch the epidemic curve in the timeline.
+
+**Key concepts:** [stochastic firing](petri-net-extensions.md#stochastic-rate), [parameters](petri-net-extensions.md#global-parameters), [arc weight](useful-patterns.md#arc-weight-for-multi-token-operations).
+
+
+
+## Supply Chain (Stochastic)
+
+A manufacturing pipeline from raw material suppliers through manufacturing, quality assurance, and shipping to a hospital. Products have a random `quality` attribute that determines whether they pass QA.
+
+**Demonstrates:**
+
+- **Typed place** -- QAQueue has a "Product" type with a `quality` dimension.
+- **`Distribution.Uniform`** in a transition kernel to sample random quality at manufacturing time.
+- **Competing predicate transitions** -- "Dispatch" fires when `quality >= threshold`, "Dispose" fires when `quality < threshold`, routing tokens to different output places.
+- **Parameter-driven guard** -- the `quality_threshold` parameter controls the pass/fail boundary.
+
+**Suggested initial state:** set PlantASupply and PlantBSupply to **10** tokens each. Everything else starts empty. The stochastic "Deliver to Plant" transition will begin consuming from both suppliers once the simulation starts.
+
+**Key concepts:** [types](petri-net-extensions.md#typed-vs-untyped-places), [distributions](petri-net-extensions.md#distributions), [competing transitions](useful-patterns.md#competing-transitions--routing).
+
+
+
+## Deployment Pipeline
+
+A software deployment process with incident handling. Deployments are created at a stochastic rate and proceed through a pipeline, but are blocked by incidents.
+
+**Demonstrates:**
+
+- **Inhibitor arcs** -- "Start Deployment" has inhibitor arcs from "IncidentBeingInvestigated" and "DeploymentInProgress", preventing new deployments while an incident is open or another deployment is running.
+- **Source transitions** -- "Create Deployment" and "Incident Raised" have no input arcs, modelling Poisson arrivals at configurable rates.
+- **Stochastic rates from parameters** -- `deployment_creation_rate`, `incident_rate`, `incident_resolution_rate`.
+
+**Suggested initial state:** no initial tokens needed -- all places can start empty. The source transitions "Create Deployment" and "Incident Raised" generate tokens at their stochastic rates. Just press Play.
+
+**Key concepts:** [inhibitor arcs](petri-net-extensions.md#inhibitor-arcs), [source transitions](useful-patterns.md#source-transitions-exogenous-arrivals), [mutual exclusion](useful-patterns.md#mutual-exclusion-with-inhibitor-arcs).
+
+
+
+## Production Machines
+
+A manufacturing system where machines produce goods, accumulate damage, break down, and are repaired by travelling technicians.
+
+**Demonstrates:**
+
+- **Multiple typed places** with three different types: Machine (`machine_damage_ratio`), MachineProducingProduct (`machine_damage_ratio`, `transformation_progress`), and Technician (`distance_to_site`).
+- **Differential equations** on three places: production progress advancing, damage being repaired, and technicians travelling to the repair site.
+- **Predicate guards** based on continuous state -- production completes when `transformation_progress >= 1`, repair finishes when `machine_damage_ratio <= 0`, technician arrives when `distance_to_site <= 0`.
+- **Competing outcomes** -- "Production Success" (predicate) vs "Machine Fail" (stochastic with rate `machine_damage_ratio ** 100`, increasing sharply with accumulated damage).
+
+**Suggested initial state:**
+
+| Place | Tokens | Values |
+| ----------------- | ------ | ------------------------------ |
+| RawMaterial | 100 | (untyped) |
+| AvailableMachines | 3 | `machine_damage_ratio: 0` each |
+
+All other places start empty. "Start Production" will immediately consume a raw material and an available machine to begin.
+
+**Key concepts:** [dynamics](petri-net-extensions.md#differential-equations-dynamics), [resource pools](useful-patterns.md#resource-pools), predicate vs stochastic on competing transitions.
+
+
+
+## Satellites in Orbit
+
+An orbital mechanics simulation with satellites orbiting Earth, subject to collision and crash events.
+
+**Demonstrates:**
+
+- **Continuous dynamics** -- gravitational ODE computes acceleration, updating satellite position (`x`, `y`) and motion (`direction`, `velocity`) each step.
+- **Custom place visualization** -- an SVG visualizer renders Earth, satellite positions, and velocity vectors in the properties panel.
+- **Predicate transitions based on geometry** -- "Collision" checks distance between two satellites, "Crash" checks distance from Earth's surface.
+- **Arc weight 2** on the "Collision" transition -- requires two satellites from the same place to evaluate pairwise proximity.
+- **Parameters** for physical constants: `earth_radius`, `satellite_radius`, `gravitational_constant`, `crash_threshold`.
+
+**Suggested initial state:** add 3--5 satellite tokens to the Space place. Position them in a rough orbit around the origin (Earth is at 0,0). For example:
+
+| x | y | direction | velocity |
+| --- | ---- | --------- | -------- |
+| 80 | 0 | 1.57 | 70 |
+| 0 | 100 | 3.14 | 55 |
+| -60 | -60 | 0.78 | 80 |
+
+The velocity needed for a roughly circular orbit at radius `r` is approximately `sqrt(gravitational_constant / r)`. With the default `gravitational_constant` of 400000, that's about 71 at radius 80. Select the Space place and open the visualizer preview to watch the orbits.
+
+**Key concepts:** [dynamics](petri-net-extensions.md#differential-equations-dynamics), [visualizers](petri-net-extensions.md#visualizer), [arc weight](useful-patterns.md#arc-weight-for-multi-token-operations).
+
+
+
+## Probabilistic Satellites Launcher
+
+Extends the Satellites example with ongoing satellite launches at a stochastic rate.
+
+**Demonstrates:**
+
+- **Source transition** with stochastic rate -- "LaunchSatellite" has no inputs and fires at a constant rate, injecting new satellites into orbit.
+- **`Distribution.Uniform` and `Distribution.Gaussian`** in the launch kernel for randomized initial conditions.
+- **`Distribution.map()`** for coordinate conversion -- a uniform angle is sampled once, then `.map()` derives both `x` (cosine) and `y` (sine) from the same underlying sample for coherent polar-to-cartesian conversion.
+
+**Suggested initial state:** no initial tokens needed -- start with all places empty. The "LaunchSatellite" source transition fires at a rate of 1 per second, creating satellites with randomized orbital positions and velocities. Just press Play and watch the Space visualizer fill up.
+
+**Key concepts:** [source transitions](useful-patterns.md#source-transitions-exogenous-arrivals), [distributions and `.map()`](petri-net-extensions.md#distributions).
+
+
diff --git a/libs/@hashintel/petrinaut/docs/petri-net-extensions.md b/libs/@hashintel/petrinaut/docs/petri-net-extensions.md
new file mode 100644
index 00000000000..2c5c3591ec6
--- /dev/null
+++ b/libs/@hashintel/petrinaut/docs/petri-net-extensions.md
@@ -0,0 +1,181 @@
+# Petri Net Extensions
+
+Petrinaut extends basic Petri nets with typed tokens, continuous dynamics, stochastic firing, and more. This page covers each extension.
+
+## Typed vs untyped places
+
+By default, places hold **untyped tokens** -- they only track a count. Tokens are indistinguishable from each other. This is sufficient for simple flow models.
+
+To give tokens structure, assign a **type** to a place. Each token then carries named dimensions (e.g. `x`, `y`, `velocity`), enabling dynamics, visualization, and data-dependent transition logic.
+
+**To create a type:**
+
+1. Open the **Token Types** tab in the left sidebar.
+2. Click **+** to add a new type.
+3. Give it a **name** and **display colour**.
+
+
+
+**To assign a type to a place:** select the place, then choose the type from the **Accepted token type** dropdown in the properties panel.
+
+Once a place has a type, its tokens are accessible in code as structured objects. For example, a type with dimensions `x` and `y` means each token is `{ x: number, y: number }`.
+
+## Global parameters
+
+Parameters are named values available in all user-authored code: dynamics, firing rate, kernels, and visualizers. They are accessed via the `parameters` argument.
+
+**To create a parameter:**
+
+1. Open the **Parameters** tab in the left sidebar.
+2. Click **+** to add a new parameter.
+3. Set a **name** (display label), **variable name** (used in code), and **default value** (can be overridden in the simulation settings).
+
+
+
+Override parameter values before running a simulation in the **Simulation Settings** panel (see [Simulation](simulation.md#simulation-settings)). This lets you experiment with different values without editing code.
+
+**Example:** the [SIR Epidemic Model](examples.md#sir-epidemic-model) defines `infection_rate` and `recovery_rate` as parameters, used in its transition lambdas.
+
+## Differential equations (dynamics)
+
+Differential equations define how token data evolves continuously over time. They are integrated at each simulation step using the Euler method.
+
+**Setup:**
+
+1. Create a differential equation in the **Differential Equations** tab (left sidebar).
+2. Give it a name and associate it with a **type** (the equation applies to tokens of that type).
+3. Select a place, enable **Dynamics**, and choose an equation that matches the type assigned to the place.
+
+**Function signature:**
+
+```ts
+export default Dynamics((tokens, parameters) => {
+ return tokens.map(({ x, y }) => {
+ return { x: /* dx/dt */, y: /* dy/dt */ };
+ });
+});
+```
+
+The function receives the current token values and global parameters. It must return an array of derivative objects -- one per token, with the same dimension names.
+
+
+
+**Example:** in [Satellites in Orbit](examples.md#satellites-in-orbit), the orbital dynamics equation computes gravitational acceleration to update satellite position and velocity each step.
+
+## Visualizer
+
+A visualizer renders a custom view of a place's tokens during simulation. It is a React component that returns JSX (SVG is recommended).
+
+**To enable:** select a place, then toggle **Visualizer** in its properties. A code editor opens.
+
+```tsx
+export default Visualization(({ tokens, parameters }) => {
+ return
+});
+```
+
+The component receives `tokens` (array of token objects) and `parameters` (global parameter values). It renders in the properties panel. During simulation, it updates live as token state changes.
+
+
+
+Use the menu in the code editor header to **Load default template** for a starting point.
+
+You can also toggle between the code, a preview, and both at once.
+
+**Example:** the [Satellites in Orbit](examples.md#satellites-in-orbit) example includes a visualizer that renders Earth and orbiting satellites with velocity vectors.
+
+## Transition kernel
+
+The transition kernel defines how input tokens are transformed into output tokens when a transition fires.
+
+```ts
+export default TransitionKernel((tokensByPlace, parameters) => {
+ return {
+ OutputPlace: [{ x: tokensByPlace.InputPlace[0].x + 1 }],
+ };
+});
+```
+
+`tokensByPlace` is keyed by **place name**. Each value is an array of token objects from that input place. The return value is keyed by **output place name**, each containing an array of token objects to produce.
+
+Use the menu in the code editor header to **Load default template** for a starting point.
+
+### Distributions
+
+Kernel output values can be numbers or `Distribution` objects for stochastic output:
+
+- `Distribution.Gaussian(mean, standardDeviation)`
+- `Distribution.Uniform(min, max)`
+- `Distribution.Lognormal(mu, sigma)`
+
+Use `.map(fn)` to transform a sampled value:
+
+```ts
+const angle = Distribution.Uniform(0, 2 * Math.PI);
+return {
+ Space: [{
+ x: angle.map(a => Math.cos(a) * 80),
+ y: angle.map(a => Math.sin(a) * 80),
+ }],
+};
+```
+
+The underlying random sample is drawn once and shared across chained `.map()` calls, so `x` and `y` above are derived from the same angle.
+
+### Empty kernels
+
+For transitions where all output places are **untyped**, the kernel code can be left empty. The engine produces the correct number of black tokens automatically.
+
+## Firing rate / predicate
+
+Each transition has a **firing rate** that controls when it fires, once structurally enabled (sufficient tokens in input places). Choose between two modes in the transition properties:
+
+### Predicate
+
+The function returns a **boolean**. The transition fires immediately when it returns `true`.
+
+```ts
+export default Lambda((tokensByPlace, parameters) => {
+ return tokensByPlace.MyPlace[0].progress >= 1.0;
+});
+```
+
+Use predicates for deterministic guards based on token state.
+
+### Stochastic rate
+
+The function returns a **number** representing the average firing rate per second:
+
+- `0` -- disabled (will not fire).
+- Any positive number -- average rate (e.g. `2.0` means roughly twice per second).
+- `Infinity` -- fires immediately when enabled.
+
+```ts
+export default Lambda((tokensByPlace, parameters) => {
+ return parameters.rate;
+});
+```
+
+## Inhibitor arcs
+
+An inhibitor arc is a special input arc that **prevents** a transition from firing when the source place has tokens equal to or greater than the arc weight -- the opposite of a normal arc.
+
+**To set:** select an input arc (place to transition) and switch its **Type** to **Inhibitor** in the properties panel. Only input arcs can be inhibitor.
+
+**Semantics:** the transition is enabled (on this arc) when the source place has **fewer tokens than the arc weight**. With the default weight of 1, this means the place must be empty.
+
+Inhibitor arcs **do not consume tokens** when the transition fires.
+
+
+
+**Example:** in [Deployment Pipeline](examples.md#deployment-pipeline), inhibitor arcs from "IncidentBeingInvestigated" and "DeploymentInProgress" block new deployments while an incident is open or a deployment is already running.
+
+## Diagnostics
+
+The **Diagnostics** tab in the bottom panel shows TypeScript errors in your code (dynamics, firing rate, kernels, visualizers), grouped by entity. Click a diagnostic to select the relevant entity and see the error in context.
+
+Diagnostics must be resolved before running a simulation -- pressing Play with unresolved errors opens the Diagnostics tab instead of starting the simulation.
diff --git a/libs/@hashintel/petrinaut/docs/simulation.md b/libs/@hashintel/petrinaut/docs/simulation.md
new file mode 100644
index 00000000000..69c5cee1286
--- /dev/null
+++ b/libs/@hashintel/petrinaut/docs/simulation.md
@@ -0,0 +1,133 @@
+# Simulation
+
+## Initial state
+
+Before running a simulation, set the **initial marking** -- the starting tokens in each place.
+
+Select a place and open the **State** sub-view in its properties:
+
+- **Untyped places** -- set a token count (integer).
+- **Typed places** -- define individual tokens with values for each dimension in a spreadsheet editor. Add a row to create a new token.
+
+
+
+If no initial marking is set, a place starts empty (zero tokens).
+
+## Simulation settings
+
+Open the **Simulation Settings** tab in the bottom panel to configure:
+
+### Parameters
+
+Override [global parameter](petri-net-extensions.md#global-parameters) values for this run. Each parameter shows its name, variable name, and a value input (pre-filled with the default). Changes here do not modify the parameter definition -- they only apply to the simulation.
+
+Parameter values are locked while a simulation is running. Reset the simulation to change them.
+
+### Time step (dt)
+
+The time step in seconds per frame. Controls the resolution of ODE integration and how frequently transitions are evaluated.
+
+- **Smaller dt** -- finer approximation, but slower computation.
+- **Larger dt** -- faster, but less accurate for continuous dynamics.
+
+Default: `0.01` seconds.
+
+### ODE solver
+
+The numerical method for integrating differential equations. Currently only **Euler** is available.
+
+## Running a simulation
+
+Press **Play** in the bottom toolbar. The simulation:
+
+1. Initializes with a random seed, the current dt, and parameter values.
+2. Computes frames in a background Web Worker.
+3. Streams frames to the UI for playback.
+
+If there are unresolved [diagnostics](petri-net-extensions.md#diagnostics) (code errors), pressing Play opens the Diagnostics tab instead of starting the simulation. Fix all errors first.
+
+
+
+## How a frame is computed
+
+Each simulation step proceeds in two phases:
+
+1. **Continuous dynamics** -- for every place with dynamics enabled, the differential equation is integrated one step (Euler method, step size = dt). This updates all token dimension values.
+
+2. **Discrete transitions** -- transitions are evaluated in definition order (deterministic, not random). For each transition:
+ - Checks structural enablement (enough tokens in input places, inhibitor conditions met).
+ - Evaluates the lambda (predicate or stochastic rate).
+ - If the transition fires, removes input tokens **immediately** (subsequent transitions see the updated state).
+
+ All produced output tokens are added at the end of the step.
+
+Simulation time advances by `dt` each frame.
+
+## Deadlock
+
+If no transition fires in a step **and** no transition is structurally enabled (regardless of lambda values), the simulation reports **deadlock** and stops (a "Simulation Complete" message is shown).
+
+This only stops computation: the simulation will continue to playback computed frames until no more are available.
+
+If transitions are structurally enabled but their lambdas prevent firing, the simulation continues stepping.
+
+## Playback controls
+
+The bottom toolbar provides playback controls:
+
+| Control | Description |
+| ---------------- | ----------------------------------- |
+| **Play** | Start or resume playback. |
+| **Pause** | Pause at the current frame. |
+| **Stop / Reset** | Stop playback and reset to frame 0. |
+
+The frame counter shows the current frame number, total frames, and elapsed simulation time.
+
+
+
+### Speed
+
+Choose a playback speed multiplier via playback settings: **1x**, **2x**, **5x**, **10x**, **30x**, **60x**, **120x**, or **Max** (as fast as possible).
+
+### Play mode
+
+Controls how computation and playback interact:
+
+| Mode | Behavior |
+| ---------------------------- | ----------------------------------------------------------- |
+| **Play computed steps only** | Replay already-computed frames without further computation. |
+| **Play + compute buffer** | Compute only a small buffer ahead of the playhead. |
+| **Play + compute max** | Compute frames as fast as possible while playing. |
+
+### Stopping condition
+
+- **Run indefinitely** -- simulation continues until manually paused or deadlock.
+- **End at fixed time** -- simulation stops after a set number of seconds (simulation time).
+
+Stopping conditions are **locked after the simulation starts**. Reset the simulation to change them.
+
+## Timeline
+
+The **Timeline** tab appears in the bottom panel during and after simulation. It shows token counts per place over time as a chart.
+
+
+
+- **Chart type** -- toggle between **Run** (line chart) and **Stacked** (area chart) using the control in the tab header.
+- **Scrub** -- click or drag on the chart to jump to any frame. A playhead indicator shows the current position.
+- **Legend** -- click place names to show/hide individual traces. Hover to dim other traces. Y axis is automatically scaled to the maximum value.
+
+## Viewing state during simulation
+
+Select a place during simulation to see its current token values in the properties panel. For typed places, individual token dimension values are displayed.
+
+If the place has a [visualizer](petri-net-extensions.md#visualizer) defined, it renders live in the properties panel, updating as the simulation progresses.
+
+
+
+## Locked editing
+
+The editor is **read-only** during simulation and after a simulation completes. You cannot add, remove, or modify nodes, arcs, types, or code while a simulation exists.
+
+Press **Stop / Reset** to return to editing mode.
+
+At the last frame of a completed simulation, Play is disabled -- reset to replay from the beginning.
diff --git a/libs/@hashintel/petrinaut/docs/useful-patterns.md b/libs/@hashintel/petrinaut/docs/useful-patterns.md
new file mode 100644
index 00000000000..a21a8d8713c
--- /dev/null
+++ b/libs/@hashintel/petrinaut/docs/useful-patterns.md
@@ -0,0 +1,140 @@
+# Useful Patterns
+
+Useful modelling techniques for Petri nets in Petrinaut.
+
+## Modelling duration (exponential)
+
+For processes with **exponentially distributed** duration, set the transition's stochastic firing rate to `1 / mean_duration`. The exponential distribution is built into the stochastic firing mechanism -- no extra setup needed.
+
+```ts
+export default Lambda((tokensByPlace, parameters) => {
+ return 1 / parameters.mean_repair_time;
+});
+```
+
+This is the simplest way to model duration and works well for many processes (service times, failure intervals, etc.).
+
+## Modelling duration (non-exponential)
+
+For other distributions (e.g. log-normal, deterministic), place dynamics and durations sampled in the preceding transition kernel can be used. The general approach:
+
+1. **Add a time dimension** to the token type (e.g. `remaining_time`).
+2. **Sample the duration** in a transition kernel using a `Distribution`:
+
+```ts
+export default TransitionKernel((tokensByPlace, parameters) => {
+ return {
+ InProgress: [{
+ remaining_time: Distribution.Lognormal(2.0, 0.5),
+ // ... other dimensions
+ }],
+ };
+});
+```
+
+1. **Count down** with a differential equation:
+
+```ts
+export default Dynamics((tokens, parameters) => {
+ return tokens.map(() => ({ remaining_time: -1 }));
+});
+```
+
+1. **Guard the completion transition** with a predicate:
+
+```ts
+export default Lambda((tokensByPlace, parameters) => {
+ return tokensByPlace.InProgress[0].remaining_time <= 0;
+});
+```
+
+**Alternative approach:** use two dimensions -- a fixed `sampled_duration` that doesn't change and a `counter` that increments via dynamics. Guard on `counter >= sampled_duration`. This preserves the original sampled value for inspection.
+
+## Resource pools
+
+Use a place as a **pool** of tokens representing limited resources (machines, workers, servers). Transitions consume from the pool when starting work and return tokens when done.
+
+**Structure:**
+
+```text
+(Available) ---> [StartWork] ---> (InUse) ---> [FinishWork] ---> [Available)
+```
+
+The number of initial tokens in "Available" determines the resource capacity. If no tokens are available, "StartWork" cannot fire -- work is naturally queued.
+
+**Example:** the [Production Machines](examples.md#production-machines) example models machines cycling between available, producing, broken, and being repaired states.
+
+## Mutual exclusion with inhibitor arcs
+
+Use an [inhibitor arc](petri-net-extensions.md#inhibitor-arcs) from a "busy" or "blocked" place to prevent a transition from firing while a condition holds.
+
+**Structure:**
+
+```text
+(Busy) ---o [StartNew] (inhibitor arc, weight 1)
+```
+
+"StartNew" can only fire when "Busy" has zero tokens. Once something enters the busy state, no new work can start until the token is removed.
+
+**Example:** the [Deployment Pipeline](examples.md#deployment-pipeline) uses inhibitor arcs to block new deployments while an incident is being investigated or another deployment is already in progress.
+
+## Source transitions (exogenous arrivals)
+
+A transition with **no input arcs** is always structurally enabled. Set a stochastic rate to model arrivals following a Poisson process.
+
+```ts
+export default Lambda((tokensByPlace, parameters) => {
+ return parameters.arrival_rate;
+});
+```
+
+Use the transition kernel to define the properties of newly created tokens (if the output place is typed).
+
+**Examples:**
+
+- [Deployment Pipeline](examples.md#deployment-pipeline) -- "Create Deployment" and "Incident Raised" generate events at configurable rates.
+- [Probabilistic Satellites Launcher](examples.md#probabilistic-satellites-launcher) -- "LaunchSatellite" creates satellites with randomized initial positions and velocities using `Distribution.Uniform` and `Distribution.Gaussian`.
+
+## Sink transitions (removal / absorption)
+
+A transition with **no output arcs** consumes tokens without producing any. Useful for modelling:
+
+- **Expiry** -- tokens that age out or are consumed.
+- **Departure** -- entities leaving the system.
+- **Disposal** -- rejected or failed items.
+
+No special configuration needed -- just create a transition with input arcs only.
+
+## Competing transitions / routing
+
+Multiple transitions consuming from the **same place** with **complementary predicates** can model routing or branching decisions.
+
+**Structure:**
+
+```text
+ /--> [Pass] ---> (Dispatched)
+(QAQueue) --<
+ \--> [Fail] ---> (Disposed)
+```
+
+```ts
+// Pass transition
+export default Lambda((tokensByPlace, parameters) => {
+ return tokensByPlace.QAQueue[0].quality >= parameters.quality_threshold;
+});
+
+// Fail transition
+export default Lambda((tokensByPlace, parameters) => {
+ return tokensByPlace.QAQueue[0].quality < parameters.quality_threshold;
+});
+```
+
+**Example:** the [Supply Chain (Stochastic)](examples.md#supply-chain-stochastic) example routes products to dispatch or disposal based on a quality threshold.
+
+## Arc weight for multi-token operations
+
+An input arc with **weight > 1** requires multiple tokens from the same place for the transition to be enabled. This is useful for interactions between entities.
+
+**Example:** the [Satellites in Orbit](examples.md#satellites-in-orbit) example has a "Collision" transition with input weight 2 from the "Space" place -- it requires two satellites to be present and checks their distance in the lambda to detect collisions.
+
+The transition kernel receives the consumed tokens and can compute outputs based on all of them.
diff --git a/libs/@hashintel/petrinaut/docs/visual-settings.md b/libs/@hashintel/petrinaut/docs/visual-settings.md
new file mode 100644
index 00000000000..38f7875d07f
--- /dev/null
+++ b/libs/@hashintel/petrinaut/docs/visual-settings.md
@@ -0,0 +1,51 @@
+# Visual Settings
+
+Access the settings dialog via the **gear icon** in the viewport controls (bottom-right corner of the canvas).
+
+
+
+## Available settings
+
+### Animations
+
+Toggle panel transition and UI interaction animations. Disable for a snappier feel or if animations cause performance issues.
+
+### Keep panels mounted
+
+When enabled, hidden panels remain loaded in the background. Switching between panels is faster, but uses more memory. When disabled, panels are unmounted when hidden and re-created when opened.
+
+### Minimap
+
+Show or hide the **overview minimap** in the top-right corner of the canvas. The minimap provides a zoomed-out view of the entire net for orientation in large models.
+
+### Snap to grid
+
+When enabled, node positions snap to a grid when placing new nodes or dragging existing ones. Helps keep nets tidy and aligned.
+
+### Compact nodes
+
+Switch between two node rendering styles:
+
+- **Compact** (enabled) -- smaller card-style nodes.
+- **Classic** (disabled) -- larger nodes with more detail.
+
+### Partial selection
+
+Controls selection box behavior in [Select mode](drawing-a-net.md#pan-and-select-modes):
+
+- **Enabled** -- nodes that are only partially inside the selection box are selected.
+- **Disabled** -- nodes must be fully enclosed to be selected.
+
+### Entities tree view (experimental)
+
+Replaces the tabbed left sidebar with a unified **tree view** showing all entities (nodes, types, equations, parameters) in a single hierarchy.
+
+### Arcs rendering
+
+Choose how arcs are drawn between nodes:
+
+| Style | Description |
+| ------------------- | -------------------------------------------------------------|
+| **Square** | Right-angle paths (smoothstep routing). |
+| **Bezier** | Smooth curved paths. |
+| **Adaptive Bezier** | Curved paths that adjust based on node positions. (Default) |
diff --git a/libs/@hashintel/petrinaut/src/examples/supply-chain.ts b/libs/@hashintel/petrinaut/src/examples/supply-chain.ts
deleted file mode 100644
index 58c0e51fefa..00000000000
--- a/libs/@hashintel/petrinaut/src/examples/supply-chain.ts
+++ /dev/null
@@ -1,128 +0,0 @@
-import { SNAP_GRID_SIZE } from "../constants/ui";
-import type { SDCPN } from "../core/types/sdcpn";
-
-export const supplyChainSDCPN: { title: string; petriNetDefinition: SDCPN } = {
- title: "Drug Production",
- petriNetDefinition: {
- places: [
- {
- id: "place__0",
- name: "PlantASupply",
- colorId: null,
- dynamicsEnabled: false,
- differentialEquationId: null,
- x: SNAP_GRID_SIZE,
- y: 8 * SNAP_GRID_SIZE,
- },
- {
- id: "place__1",
- name: "PlantBSupply",
- colorId: null,
- dynamicsEnabled: false,
- differentialEquationId: null,
- x: SNAP_GRID_SIZE,
- y: 40 * SNAP_GRID_SIZE,
- },
- {
- id: "place__2",
- name: "ManufacturingPlant",
- colorId: null,
- dynamicsEnabled: false,
- differentialEquationId: null,
- x: 20 * SNAP_GRID_SIZE,
- y: 20 * SNAP_GRID_SIZE,
- },
- {
- id: "place__3",
- name: "QAQueue",
- colorId: null,
- dynamicsEnabled: false,
- differentialEquationId: null,
- x: 47 * SNAP_GRID_SIZE,
- y: 23 * SNAP_GRID_SIZE,
- },
- {
- id: "place__4",
- name: "Disposal",
- colorId: null,
- dynamicsEnabled: false,
- differentialEquationId: null,
- x: 73 * SNAP_GRID_SIZE,
- y: 40 * SNAP_GRID_SIZE,
- },
- {
- id: "place__5",
- name: "Dispatch",
- colorId: null,
- dynamicsEnabled: false,
- differentialEquationId: null,
- x: 67 * SNAP_GRID_SIZE,
- y: 13 * SNAP_GRID_SIZE,
- },
- {
- id: "place__6",
- name: "Hospital",
- colorId: null,
- dynamicsEnabled: false,
- differentialEquationId: null,
- x: 87 * SNAP_GRID_SIZE,
- y: 25 * SNAP_GRID_SIZE,
- },
- ],
- transitions: [
- {
- id: "transition__0",
- name: "Deliver to Plant",
- inputArcs: [
- { placeId: "place__0", weight: 1, type: "standard" },
- { placeId: "place__1", weight: 1, type: "standard" },
- ],
- outputArcs: [{ placeId: "place__2", weight: 1 }],
- lambdaType: "predicate",
- lambdaCode: "export default Lambda(() => true);",
- transitionKernelCode: "",
- x: 7 * SNAP_GRID_SIZE,
- y: 27 * SNAP_GRID_SIZE,
- },
- {
- id: "transition__1",
- name: "Manufacture",
- inputArcs: [{ placeId: "place__2", weight: 1, type: "standard" }],
- outputArcs: [{ placeId: "place__3", weight: 1 }],
- lambdaType: "predicate",
- lambdaCode: "export default Lambda(() => true);",
- transitionKernelCode: "",
- x: 33 * SNAP_GRID_SIZE,
- y: 23 * SNAP_GRID_SIZE,
- },
- {
- id: "transition__2",
- name: "Quality Check",
- inputArcs: [{ placeId: "place__3", weight: 1, type: "standard" }],
- outputArcs: [
- { placeId: "place__5", weight: 1 },
- { placeId: "place__4", weight: 1 },
- ],
- lambdaType: "predicate",
- lambdaCode: "export default Lambda(() => true);",
- transitionKernelCode: "",
- x: 58 * SNAP_GRID_SIZE,
- y: 27 * SNAP_GRID_SIZE,
- },
- {
- id: "transition__3",
- name: "Ship",
- inputArcs: [{ placeId: "place__5", weight: 1, type: "standard" }],
- outputArcs: [{ placeId: "place__6", weight: 1 }],
- lambdaType: "predicate",
- lambdaCode: "export default Lambda(() => true);",
- transitionKernelCode: "",
- x: 77 * SNAP_GRID_SIZE,
- y: 19 * SNAP_GRID_SIZE,
- },
- ],
- types: [],
- differentialEquations: [],
- parameters: [],
- },
-};
diff --git a/libs/@hashintel/petrinaut/src/views/Editor/editor-view.tsx b/libs/@hashintel/petrinaut/src/views/Editor/editor-view.tsx
index c4860a2d6d4..541f6045436 100644
--- a/libs/@hashintel/petrinaut/src/views/Editor/editor-view.tsx
+++ b/libs/@hashintel/petrinaut/src/views/Editor/editor-view.tsx
@@ -8,7 +8,6 @@ import { deploymentPipelineSDCPN } from "../../examples/deployment-pipeline";
import { satellitesSDCPN } from "../../examples/satellites";
import { probabilisticSatellitesSDCPN } from "../../examples/satellites-launcher";
import { sirModel } from "../../examples/sir-model";
-import { supplyChainSDCPN } from "../../examples/supply-chain";
import { supplyChainStochasticSDCPN } from "../../examples/supply-chain-stochastic";
import { exportSDCPN } from "../../file-format/export-sdcpn";
import { importSDCPN } from "../../file-format/import-sdcpn";
@@ -271,14 +270,6 @@ export const EditorView = ({
id: "load-example",
label: "Load example",
submenu: [
- {
- id: "load-example-supply-chain",
- label: "Supply Chain",
- onClick: () => {
- createNewNet(supplyChainSDCPN);
- clearSelection();
- },
- },
{
id: "load-example-supply-chain-stochastic",
label: "Probabilistic Supply Chain",
@@ -331,6 +322,17 @@ export const EditorView = ({
},
]
: []),
+ {
+ id: "docs",
+ label: "Docs",
+ onClick: () => {
+ window.open(
+ "https://github.com/hashintel/hash/tree/main/libs/%40hashintel/petrinaut/docs",
+ "_blank",
+ "noopener,noreferrer",
+ );
+ },
+ },
];
const portalContainerRef = useRef(null);