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-basenpm install @snap-engine/snapline @snap-engine/asset-base react react-domnpm install @snap-engine/snaplineFirst 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.