Installation and setup
Install SnapLine and render a first node graph in Vanilla, Svelte, or React.
Install the core package plus the adapter you use.
npm install @snap-engine/core @snap-engine/snapline npm install @snap-engine/asset-base-svelte @snap-engine/snapline-sveltenpm install react react-dom @snap-engine/snapline-reactFirst graph
<script lang="ts">
import { Engine } from "@snap-engine/asset-base-svelte";
import {
Connector,
ControlledGraph,
Node,
Select,
} from "@snap-engine/snapline-svelte";
import type { LineChangeRequest, LineRecord } from "@snap-engine/snapline";
// Your document owns the lines; each gesture proposes one atomic change.
let lines = $state<LineRecord[]>([]);
function applyRequest(request: LineChangeRequest) {
lines = [
...lines
.filter((record) => !request.remove.includes(record.id))
.map((record) => {
const update = request.update.find((u) => u.id === record.id);
return update
? { ...record, toConnectorId: update.toConnectorId }
: record;
}),
...request.add,
];
}
</script>
<Engine>
<Select />
<ControlledGraph {lines} 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 { useCallback, useState } from "react";
import {
Connector,
ControlledGraph,
Engine,
Node,
Select,
} from "@snap-engine/snapline-react";
import type { LineChangeRequest, LineRecord } from "@snap-engine/snapline";
export function Graph() {
// Your document owns the lines; each gesture proposes one atomic change.
const [lines, setLines] = useState<LineRecord[]>([]);
const applyRequest = useCallback((request: LineChangeRequest) => {
setLines((current) => [
...current
.filter((record) => !request.remove.includes(record.id))
.map((record) => {
const update = request.update.find((u) => u.id === record.id);
return update
? { ...record, toConnectorId: update.toConnectorId }
: record;
}),
...request.add,
]);
}, []);
return (
<Engine>
<Select />
<ControlledGraph lines={lines} 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,
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,
// apply each atomic proposal, and push the latest snapshot back.
let lines: LineRecord[] = [];
const handle = attachControlledGraph(engine, {
onLineChangeRequest(request) {
lines = [
...lines
.filter((record) => !request.remove.includes(record.id))
.map((record) => {
const update = request.update.find((u) => u.id === record.id);
return update
? { ...record, toConnectorId: update.toConnectorId }
: record;
}),
...request.add,
];
handle.setCanonicalGraph({ lines });
},
});
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.