Installation and setup

On this page

SnapLine ships its framework-neutral core and framework bindings in one package. Install the shared Engine binding for your framework alongside it.

npm install @snap-engine/snapline @snap-engine/asset-base
npm install @snap-engine/snapline @snap-engine/asset-base react react-dom
npm install @snap-engine/snapline

First graph

<script lang="ts">
  import { Engine } from "@snap-engine/asset-base/svelte";
  import {
    Connector,
    ControlledGraph,
    Node,
    Select,
  } from "@snap-engine/snapline/svelte";
  import { applyLineChange } from "@snap-engine/snapline";
  import type { LineRecord } from "@snap-engine/snapline";

  // Your document owns the lines; each gesture proposes one atomic change.
  // Return the list that should now be canonical — `$state.raw` because every
  // update replaces the array rather than mutating it.
  let lines = $state.raw<LineRecord[]>([]);
</script>

<Engine>
  <Select />
  <ControlledGraph
    onLineChangeRequest={(request) => (lines = applyLineChange(lines, request))}
  />
  <Node id="source" x={80} y={90} className="node">
    <strong>Source</strong>
    <Connector id="source:value" name="value" rules={{ maxIncoming: 0 }} />
  </Node>
  <Node id="result" x={360} y={180} className="node">
    <strong>Result</strong>
    <Connector id="result:value" name="value" rules={{ maxOutgoing: 0, maxIncoming: 1 }} />
  </Node>
</Engine>
import { useCallback, useRef } from "react";
import {
  Connector,
  ControlledGraph,
  Engine,
  Node,
  Select,
} from "@snap-engine/snapline/react";
import { applyLineChange } from "@snap-engine/snapline";
import type { LineChangeRequest, LineRecord } from "@snap-engine/snapline";

export function Graph() {
  // Your document owns the lines; each gesture proposes one atomic change.
  // The handler must return the next list synchronously, so keep the records
  // in a ref (a functional setState would not have produced it in time) and
  // mirror them into component state only if you render them.
  const linesRef = useRef<LineRecord[]>([]);
  const applyRequest = useCallback(
    (request: LineChangeRequest) =>
      (linesRef.current = applyLineChange(linesRef.current, request)),
    [],
  );
  return (
    <Engine>
      <Select />
      <ControlledGraph onLineChangeRequest={applyRequest} />
      <Node id="source" x={80} y={90} className="node">
        <strong>Source</strong>
        <Connector id="source:value" name="value" rules={{ maxIncoming: 0 }} />
      </Node>
      <Node id="result" x={360} y={180} className="node">
        <strong>Result</strong>
        <Connector id="result:value" name="value" rules={{ maxOutgoing: 0, maxIncoming: 1 }} />
      </Node>
    </Engine>
  );
}
import { Engine } from "@snap-engine/core";
import { CollisionEngine } from "@snap-engine/core/collision";
import {
  ConnectorMirror,
  NodeMirror,
  applyLineChange,
  attachControlledGraph,
  type LineRecord,
} from "@snap-engine/snapline";

const engine = new Engine();
engine.setCollisionEngine(new CollisionEngine());
engine.assignDom(document.querySelector("#graph")!);

// A vanilla app owns the graph with a plain module: hold the records and
// return the next list from each atomic proposal.
let lines: LineRecord[] = [];
const handle = attachControlledGraph(engine, {
  onLineChangeRequest: (request) => (lines = applyLineChange(lines, request)),
});

// Only changes with no originating request need an explicit push — loading a
// saved graph, undo/redo, a collaborator's edit.
handle.setCanonicalGraph({ lines });

const source = new NodeMirror(engine, null, { id: "source" });
source.element = document.querySelector("#source")!;
source.worldTransform = { x: 80, y: 90 };
source.remeasureDomGeometry();

const output = new ConnectorMirror(engine, source, {
  id: "source:value",
  name: "value",
  rules: { maxIncoming: 0 },
});
source.addConnectorObject(output);

Adapters create and destroy core objects automatically unless you supply an existing object. Topology is always controlled: your line records are the single source of truth, gestures arrive as one atomic LineChangeRequest each, and adopting a proposed line’s id settles the dragged line in place. Vanilla integrations assign committed DOM elements and call remeasureDomGeometry() after layout.