Skip to content
Merged
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
156 changes: 0 additions & 156 deletions libs/@hashintel/petrinaut/src/ui/lib/viewport.test.ts

This file was deleted.

1 change: 0 additions & 1 deletion libs/@hashintel/petrinaut/src/ui/petrinaut.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import "@fontsource-variable/inter";
import "@fontsource-variable/inter-tight";
import "@fontsource-variable/jetbrains-mono";
import "@xyflow/react/dist/style.css";
import "./index.css";
import { type FunctionComponent, useEffect, useMemo, useRef } from "react";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { EditorContext } from "../../../../../react/state/editor-context";
import { SDCPNContext } from "../../../../../react/state/sdcpn-context";
import { useIsReadOnly } from "../../../../../react/state/use-is-read-only";
import { UserSettingsContext } from "../../../../../react/state/user-settings-context";
import { writeDraggedNodeKind } from "../../../shared/canvas-node-drag";
import { ToolbarButton } from "./toolbar-button";
import { ToolbarDivider } from "./toolbar-divider";

Expand Down Expand Up @@ -202,7 +203,7 @@ export const ToolbarModes: React.FC<ToolbarModesProps> = ({
onDragStart={(event) => {
// eslint-disable-next-line no-param-reassign
event.dataTransfer.effectAllowed = "move";
event.dataTransfer.setData("application/reactflow", "place");
writeDraggedNodeKind(event.dataTransfer, "place");
}}
>
<Icon name="circlePlus" />
Expand All @@ -216,7 +217,7 @@ export const ToolbarModes: React.FC<ToolbarModesProps> = ({
onDragStart={(event) => {
// eslint-disable-next-line no-param-reassign
event.dataTransfer.effectAllowed = "move";
event.dataTransfer.setData("application/reactflow", "transition");
writeDraggedNodeKind(event.dataTransfer, "transition");
}}
>
<Icon name="squarePlus" />
Expand Down
59 changes: 59 additions & 0 deletions libs/@hashintel/petrinaut/src/ui/views/SDCPN/canvas-renderer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* The contract between the canvas view and a renderer. A renderer draws a
* {@link CanvasScene}, owns the viewport, and publishes a
* {@link CanvasController} for the shared overlays and hooks around it.
* Interaction semantics live in the shared `useCanvasInteractions`, so a
* renderer only turns its own hit testing and gestures into those calls.
*/

import { createContext, use } from "react";

import type { ViewportAction } from "../../types/viewport-action";
import type { CanvasPoint, CanvasScene } from "./canvas-scene";
import type { Size } from "@hashintel/petrinaut-core";

/** Screen = scene × zoom + (x, y), in canvas pixels. */
export type CanvasViewport = { x: number; y: number; zoom: number };

export type CanvasController = {
getViewport: () => CanvasViewport;
/** `animate` eases the move when the renderer supports it. */
setViewport: (
viewport: CanvasViewport,
options?: { animate?: boolean },
) => void;
zoomIn: () => void;
zoomOut: () => void;
/** Client (viewport-relative screen) coordinates to scene coordinates. */
screenToScene: (point: CanvasPoint) => CanvasPoint;
sceneToScreen: (point: CanvasPoint) => CanvasPoint;
};

export const CanvasControllerContext = createContext<CanvasController | null>(
null,
);

/** The controller of the renderer this component is rendered inside. */
export const useCanvasController = (): CanvasController => {
const controller = use(CanvasControllerContext);
if (!controller) {
throw new Error(
"useCanvasController must be used inside a canvas renderer",
);
}
return controller;
};

export type CanvasRendererProps = {
scene: CanvasScene;
/** Settled size of the canvas container. */
containerSize: Size;
/** Extra buttons hosts add to the viewport controls. */
viewportActions?: ViewportAction[];
};

export type CanvasRenderer = React.FC<CanvasRendererProps>;

export const canvasRendererNames = ["react-flow"] as const;

export type CanvasRendererName = (typeof canvasRendererNames)[number];
10 changes: 10 additions & 0 deletions libs/@hashintel/petrinaut/src/ui/views/SDCPN/canvas-renderers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ReactFlowCanvas } from "./renderers/react-flow/react-flow-canvas";

import type { CanvasRenderer, CanvasRendererName } from "./canvas-renderer";

/** Every renderer the canvas view can mount, by name. */
export const canvasRenderers: Record<CanvasRendererName, CanvasRenderer> = {
"react-flow": ReactFlowCanvas,
};

export const defaultCanvasRenderer: CanvasRendererName = "react-flow";
137 changes: 137 additions & 0 deletions libs/@hashintel/petrinaut/src/ui/views/SDCPN/canvas-scene.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { describe, expect, it } from "vitest";

import {
compactNodeDimensions,
DEFAULT_PETRINAUT_EXTENSIONS,
generateArcId,
getArcEndpointKey,
} from "@hashintel/petrinaut-core";

import { buildCanvasScene, type CanvasSceneInput } from "./canvas-scene";

import type { Place, SDCPN, Transition } from "@hashintel/petrinaut-core";

const place = (id: string, x: number, y: number): Place => ({
id,
name: id,
colorId: null,
dynamicsEnabled: false,
differentialEquationId: null,
x,
y,
});

const transitionX = 300;
const transitionY = 40;

const transition = (
id: string,
inputs: string[],
outputs: string[],
): Transition => ({
id,
name: id,
inputArcs: inputs.map((placeId) => ({ placeId, weight: 2, type: "read" })),
outputArcs: outputs.map((placeId) => ({ placeId, weight: 1 })),
lambdaType: "predicate",
lambdaCode: "",
transitionKernelCode: "",
x: transitionX,
y: transitionY,
});

const sdcpn: SDCPN = {
places: [place("p1", 0, 0), place("p2", 600, 0)],
transitions: [transition("t1", ["p1"], ["p2"])],
types: [],
differentialEquations: [],
parameters: [],
componentInstances: [],
};

const placeKey = (placeId: string) =>
getArcEndpointKey({ kind: "place", placeId });
const inputArcId = generateArcId({ inputId: placeKey("p1"), outputId: "t1" });
const outputArcId = generateArcId({ inputId: "t1", outputId: placeKey("p2") });

const input: CanvasSceneInput = {
net: { ...sdcpn, componentInstances: [] },
sdcpn,
extensions: DEFAULT_PETRINAUT_EXTENSIONS,
dimensions: compactNodeDimensions,
draggingStateByNodeId: {},
isSelected: () => false,
isHovered: () => false,
isDimmed: () => false,
};

describe("buildCanvasScene", () => {
it("sizes nodes by kind and centres them on their stored position", () => {
const { nodes } = buildCanvasScene(input);
const p1 = nodes.find((node) => node.id === "p1")!;
const t1 = nodes.find((node) => node.id === "t1")!;

expect(p1).toMatchObject({
kind: "place",
position: { x: 0, y: 0 },
...compactNodeDimensions.place,
dragging: false,
});
expect(t1).toMatchObject({
kind: "transition",
position: { x: transitionX, y: transitionY },
...compactNodeDimensions.transition,
});
});

it("follows the drag preview while a node is dragged", () => {
const { nodes } = buildCanvasScene({
...input,
draggingStateByNodeId: {
p1: { dragging: true, position: { x: 50, y: 60 } },
},
});
expect(nodes.find((node) => node.id === "p1")).toMatchObject({
position: { x: 50, y: 60 },
dragging: true,
});
});

it("carries selection, hover and dimming per item", () => {
const { nodes, arcs } = buildCanvasScene({
...input,
isSelected: (id) => id === "p1",
isHovered: (id) => id === "t1",
isDimmed: (id) => id === "p2" || id === outputArcId,
});
expect(nodes.find((node) => node.id === "p1")?.selected).toBe(true);
expect(nodes.find((node) => node.id === "t1")?.hovered).toBe(true);
expect(nodes.find((node) => node.id === "p2")?.dimmed).toBe(true);
expect(arcs.find((arc) => arc.id === outputArcId)?.dimmed).toBe(true);
expect(arcs.find((arc) => arc.id === inputArcId)?.dimmed).toBe(false);
});

it("builds one arc per input and output arc, oriented through the transition", () => {
const { arcs } = buildCanvasScene(input);
expect(arcs).toHaveLength(2);

const inputArc = arcs.find((arc) => arc.targetId === "t1")!;
expect(inputArc).toMatchObject({
id: inputArcId,
sourceId: "p1",
transitionId: "t1",
kind: "read",
weight: 2,
sourcePortId: null,
targetPortId: null,
});

const outputArc = arcs.find((arc) => arc.sourceId === "t1")!;
expect(outputArc).toMatchObject({
id: outputArcId,
targetId: "p2",
kind: "standard",
weight: 1,
});
});
});
Loading
Loading