Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/petrinaut-preview-quick-simulation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@hashintel/petrinaut": patch
---

Add an optional Quick Simulation mode to `PetrinautPreview`: hosts supply
precompiled HIR artifacts and bounded run settings, and the preview gains
scenario configuration, compact playback controls, and an expandable timeline
reusing the editor's simulation components.
30 changes: 28 additions & 2 deletions libs/@hashintel/petrinaut/docs/preview.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,31 @@ the canvas so the canvas remains usable.
Use the compact net selector to move between the root net and its subnets. The
canvas, selection, and inspector update together when you change nets.

## Quick Simulation

Some embeds include Quick Simulation. Open **Quick Simulation** in the header
to choose one of the model's named scenarios and adjust the parameters the
embed makes available. The first scenario is selected when the embed does not
specify a valid one. There is no "No scenario" option in this surface.

The selected scenario's initial marking appears on the shared canvas before a
run starts. Press **Play** in the compact bar at the bottom to start the run;
the preview never starts it automatically. The same bar lets you pause, reset,
choose from the playback speeds allowed by the embed, and scrub through the
frames that have been produced.

As soon as frames arrive, the compact bar expands upward to show a small
timeline. The timeline follows playback, lets you hover to inspect a series,
and supports clicking or dragging to scrub the canvas to another frame. It
collapses again when you reset the simulation. This expansion is animated when
Petrinaut animations are enabled and reduced-motion is not requested.

Quick Simulation uses a fixed time step and time horizon selected by the embed.
It intentionally does not expose the full Simulate workspace, timeline-series
configuration, metric authoring, or controls for changing those run settings.
Scenario parameters are limited to the safe ranges chosen for that embedded
example.

## Navigation and embedding

The application hosting `PetrinautPreview` owns navigation. It can reflect the
Expand All @@ -37,5 +62,6 @@ markup, content-security policy, sandbox permissions, and any other embedding
or security headers.

The preview intentionally omits source code, editing tools, mode and document
management controls, experiments, optimizations, and the AI assistant. Use the
full Petrinaut interface when those workflows are needed.
management controls, experiments, optimizations, and the AI assistant. Quick
Simulation is available only when the embed supplies it. Use the full Petrinaut
interface when the omitted workflows are needed.
1 change: 1 addition & 0 deletions libs/@hashintel/petrinaut/src/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@
export type { PetrinautPreviewNavigationState } from "./ui/preview/navigation-adapter";
export { PetrinautPreview } from "./ui/preview/petrinaut-preview";
export type { PetrinautPreviewProps } from "./ui/preview/petrinaut-preview";
export type { PetrinautPreviewQuickSimulation } from "./ui/preview/quick-simulation";
export type { PetrinautNavigationController } from "./react/navigation";
export type { ViewportAction } from "./ui/types/viewport-action";
5 changes: 5 additions & 0 deletions libs/@hashintel/petrinaut/src/react/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@ export type {
NotificationTone,
} from "./notifications/context";
export { NotificationsProvider } from "./notifications/provider";
export { SimulationProvider } from "./simulation/provider";
export type {
SimulationCompiler,
SimulationProviderProps,
} from "./simulation/provider";

// --- Error tracker DI ---
export { ErrorTrackerContext } from "./error-tracker-context";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { UndoRedoContext } from "./state/undo-redo-context";
import { UserSettingsProvider } from "./state/user-settings-provider";
import { useHandleHistoryAsUndoRedo } from "./use-handle-history-as-undo-redo";

import type { Petrinaut } from "@hashintel/petrinaut-core";
import type { Petrinaut, PlaybackSpeed } from "@hashintel/petrinaut-core";
import type { ReactNode } from "react";

export type PetrinautDocumentProviderProps = {
Expand Down Expand Up @@ -57,6 +57,7 @@ export const PetrinautDocumentProvider: React.FC<

export type PetrinautCanvasProviderProps = {
children: ReactNode;
initialPlaybackSpeed?: PlaybackSpeed;
};

/**
Expand All @@ -66,8 +67,8 @@ export type PetrinautCanvasProviderProps = {
*/
export const PetrinautCanvasProvider: React.FC<
PetrinautCanvasProviderProps
> = ({ children }) => (
<PlaybackProvider>
> = ({ children, initialPlaybackSpeed }) => (
<PlaybackProvider initialSpeed={initialPlaybackSpeed}>
<ActiveNetProvider>
<EditorProvider>
<ExecutionFrameProvider>{children}</ExecutionFrameProvider>
Expand Down
22 changes: 20 additions & 2 deletions libs/@hashintel/petrinaut/src/react/playback/provider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
import {
DEFAULT_COMPUTE_MODE,
PlaybackContext,
type PlaybackSpeed,
type PlaybackContextValue,
} from "./context";
import { PlaybackProvider } from "./provider";
Expand Down Expand Up @@ -129,13 +130,15 @@ const PlaybackContextConsumer = ({
// Component wrapper for testing - defined outside to avoid closure issues with React Compiler
const TestWrapper = ({
simContext,
initialSpeed,
onContextValue,
}: {
simContext: SimulationContextValue;
initialSpeed?: PlaybackSpeed;
onContextValue: (value: PlaybackContextValue) => void;
}) => (
<SimulationContext.Provider value={simContext}>
<PlaybackProvider>
<PlaybackProvider initialSpeed={initialSpeed}>
<PlaybackContextConsumer onContextValue={onContextValue} />
</PlaybackProvider>
</SimulationContext.Provider>
Expand All @@ -145,7 +148,10 @@ const TestWrapper = ({
* Renders the PlaybackProvider with a mock SimulationContext and returns
* a function to get the current PlaybackContext value.
*/
function renderPlaybackProvider(simulationContext: SimulationContextValue): {
function renderPlaybackProvider(
simulationContext: SimulationContextValue,
initialSpeed?: PlaybackSpeed,
): {
getPlaybackValue: () => PlaybackContextValue;
renderResult: RenderResult;
rerender: (newSimulationContext: SimulationContextValue) => void;
Expand All @@ -159,6 +165,7 @@ function renderPlaybackProvider(simulationContext: SimulationContextValue): {
const renderResult = render(
<TestWrapper
simContext={simulationContext}
initialSpeed={initialSpeed}
onContextValue={captureValue}
/>,
);
Expand All @@ -170,6 +177,7 @@ function renderPlaybackProvider(simulationContext: SimulationContextValue): {
renderResult.rerender(
<TestWrapper
simContext={newSimulationContext}
initialSpeed={initialSpeed}
onContextValue={captureValue}
/>,
);
Expand Down Expand Up @@ -227,6 +235,16 @@ describe("PlaybackProvider", () => {
expect(playbackValue.isComputeAvailable).toBe(true);
});

it("should honor a host-provided initial playback speed", () => {
const simulationContext = createMockSimulationContext();
const { getPlaybackValue } = renderPlaybackProvider(
simulationContext,
10,
);

expect(getPlaybackValue().playbackSpeed).toBe(10);
});

it("should have viewOnly available when there are frames", () => {
const simulationContext = createMockSimulationContext(
{
Expand Down
10 changes: 8 additions & 2 deletions libs/@hashintel/petrinaut/src/react/playback/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,13 @@ function toComputePlayMode(mode: PlayMode): ComputePlayMode {
return mode;
}

type PlaybackProviderProps = React.PropsWithChildren;
type PlaybackProviderProps = React.PropsWithChildren<{
initialSpeed?: PlaybackSpeed;
}>;

export const PlaybackProvider: React.FC<PlaybackProviderProps> = ({
children,
initialSpeed,
}) => {
const {
dt,
Expand All @@ -84,7 +87,10 @@ export const PlaybackProvider: React.FC<PlaybackProviderProps> = ({
// Pure timing model lives in /core. The provider drives ticks via rAF and
// coordinates simulation lifecycle (init / run / pause / ack / backpressure).
const [playback] = useState<Playback>(() =>
createPlayback({ mode: playMode }),
createPlayback({
mode: playMode,
...(initialSpeed === undefined ? {} : { speed: initialSpeed }),
}),
);
// Playback only owns in-memory state; its rAF and store subscriptions are
// cleaned up by their respective effects. Disposing this handle from an
Expand Down
22 changes: 22 additions & 0 deletions libs/@hashintel/petrinaut/src/react/simulation/provider.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { describe, expect, test } from "vitest";

import { getEffectiveSelectedScenarioId } from "./provider";

import type { Scenario } from "@hashintel/petrinaut-core";

const scenarios = [{ id: "first" }, { id: "second" }] as Scenario[];

describe("effective simulation scenario", () => {
test("preserves the full editor's explicit no-scenario selection", () => {
expect(getEffectiveSelectedScenarioId(scenarios, null)).toBeNull();
});

test("defaults missing and stale selections to the first scenario", () => {
expect(getEffectiveSelectedScenarioId(scenarios, undefined)).toBe("first");
expect(getEffectiveSelectedScenarioId(scenarios, "stale")).toBe("first");
});

test("defaults an explicit no-scenario selection when one is required", () => {
expect(getEffectiveSelectedScenarioId(scenarios, null, true)).toBe("first");
});
});
Loading
Loading