Skip to main content

fl_nodes_v2 1.5.0 copy "fl_nodes_v2: ^1.5.0" to clipboard
fl_nodes_v2: ^1.5.0 copied to clipboard

A node graph editor for Flutter: pan, zoom, wire and run graphs, with ordinary Flutter widgets inside every node.

Changelog #

What a host can do with each version, newest first. The reasoning behind a change lives beside the code in CLAUDE.md; this file only says what changed.

1.5.0 #

The minimap takes the host's colours.

Added #

  • The minimap's colours are the host's to set. MinimapConfig.palette (null, the panel picks by the canvas background) takes a MinimapPalette — the panel, its bar, the writing on it, its edge, the wash, a plain node and the viewport frame — and the panel uses it as it is.
  • MinimapPalette is exported, with its two built-in palettes, MinimapPalette.onDark and MinimapPalette.onLight, and copyWith, so a host can start from one and change a colour.

1.4.0 #

Level of detail: a board zoomed out far enough is drawn from sketches the host describes, in one pass, instead of a widget per node — about ten times less raster work with thousands of nodes on screen. Off until a host asks for it. And panning and zooming no longer slow down with the number of port handles in view.

Added #

  • Level of detail. NodeEditor.sketches (null, off): past NodeSketches.below (0.25), every node is drawn from a NodeSketch the host describes — boxes (SketchBox, with an optional border), bars and SketchText laid out into a bar per line — instead of being built, in one pass. Parts fade out as they get too small to see. A node being dragged, resized or holding the focus stays built, and a sketch still selects, drags and opens its menu.
  • NodeSketches.dither (0.08): across that much zoom above the switch, the sketches are dithered in over the cards rather than replacing them in one frame.
  • NodeSketches.mountsPerFrame (48): coming back in, cards are built a few a frame, nearest the middle first, with their sketches standing in.
  • NodeSketches.refresh: a Listenable that drops every cached sketch, for what a sketch reads besides its node.

Fixed #

  • Panning and zooming no longer slow down with the number of handles on screen. A frame cost the square of the ports in view: about 66 ms with 70 nodes showing and over half a second with 170, between 25% zoom and 100%.

1.3.1 #

Trackpads and touch screens that move the canvas the way the fingers do: one pan for one swipe, a pinch that zooms about the fingers on every platform, and nothing under the cursor or the fingers selected or dragged along the way. No API changes.

Fixed #

  • A trackpad pans and zooms the canvas once on native desktop. A two-finger swipe moved the canvas twice as far as the fingers, a pinch let the board slide out from under them, and a swipe after clicking a handle dragged a wire out of it.
  • A pinch on Windows zooms about the fingers. It zoomed about the mouse cursor and threw the view sideways by hundreds of pixels on a wide window.
  • A trackpad over a node, a handle, a group or the minimap only moves the canvas. With the cursor resting on one, a swipe or pinch also selected or dragged the node, pulled a wire out of the handle, or carried the panel away.
  • A two-finger pinch only zooms, wherever the fingers land. Over a card it dragged and selected the card; now it zooms about the fingers there as on empty canvas, and a second finger takes back a drag the first had begun.

1.3.0 #

Wires that take the colour of what they carry, nodes a graph cannot do without, and handles that stay under the card covering them — from four issues a host opened. Additive: a host that sets none of the new options sees what it saw in 1.2.2, except that overlapping nodes now stack in a fixed order and a covered handle is covered.

Added #

  • NodeEditor.connectionAppearance: how each wire is drawn — its colour, width and dash — asked at paint time with the connection and the two ports it joins, so a wire can take the colour of the type it carries. Nothing is stored and nothing reaches the history. Returns a ConnectionAppearance; any field left null keeps the connection's own colour or the theme's. ConnectionDash.dashed and .dotted are ready-made patterns. The minimap is unchanged.
  • NodeDefinition.deletable and copyable (both true): a node the graph cannot do without stays selectable and draggable, but Delete and Cut leave it in place, and Copy, Cut, Duplicate and Paste never make a second one. Its wires and any frame around it are left out of a copy with it. The menus grey out an entry that would do nothing. The host's own removeNodes is unaffected.

Changed #

  • A copy that takes nothing empties the clipboard. Copying a selection made only of nodes that may not be copied used to leave the previous copy in place, so the next paste brought back something unrelated. The system clipboard is emptied too when copies are mirrored there. Copying with nothing selected still changes nothing.

Fixed #

  • A node's handles stay under a card drawn over it. They used to be drawn above every card, and a press on the top card where one lay started a wire from the node underneath. They are hidden there now and cannot be grabbed through it; a handle still reads over its own card and over any card beneath it.
  • Overlapping nodes stack in document order, the one added later on top, and a click lands on the card that is drawn on top. The order among unselected nodes used to be left to chance, and drawing and clicking could disagree about it. A selected node is still raised above the rest.
  • pub.dev links the repository and the live demo again. Both moved to ConnectingTheDots-DCWK; the demo's old address is a 404, and the old repository address no longer verified, so pub.dev showed no repository link.

1.2.2 #

A hotfix: a laptop trackpad works in a browser, and the editor's own chrome survives a large OS text size. No API changes. 1.2.1 was never published; its fixes are here.

Fixed #

  • On the web, scrolling pans the canvas: a two-finger swipe, and a mouse wheel with it, since a browser gives no reliable way to tell the two apart. Hold command or control to zoom with a scroll instead. Native desktop is unchanged — the wheel zooms and the trackpad pans.
  • A pinch zooms the canvas on the web, about the pointer. It did nothing: a browser reports a pinch as a control-wheel, which reaches Flutter as a scale signal the canvas did not listen for. Control + mouse wheel on the web zooms for the same reason.
  • The example's page sets overscroll-behavior: none, so a sideways swipe pans the demo rather than going back a page. A host serving the editor on the web wants the same line in its own index.html.
  • A group's name is no longer cut off when the reader's text scale is above 1.0. The handle zooms with the canvas, so its name is laid out at the handle's own size, the way a node's body is; the colour menu still follows the reader.
  • The minimap's bar grows with the reader's text scale, so its title is not cut off either. MinimapController.barHeight is still the height at 1.0, and a folded panel is the taller bar.

Changed #

  • A selection rectangle held against the edge scrolls the canvas, the way a wire, a node or a group already did, and keeps selecting what it reaches. Its starting corner stays where it was in the graph. Off with the rest of the edge scroll, edgeScroll: null.

1.2.0 #

The three words a host could set but not translate now fall back to the localizations: the minimap's title and Size choices, and the link-label dialog's heading. Additive: a host that sets none sees the English it saw in 1.1.0, and one that sets its own keeps it in every locale.

Added #

  • NodeEditorLocalizations.minimapTitle ('Minimap') and minimapSizePresetLabel(size) (the label from MinimapConfig.defaultSizePresets), in DefaultNodeEditorLocalizations.
  • MinimapConfig.titleIn(words) and sizePresetsIn(words), what the panel draws: the host's own, or the words given.
  • EditableLinkLabel.editorTitleIn(words), and words (const DefaultNodeEditorLocalizations()) on NodeDefinitionRegistry.editorTitleFor.

Changed #

  • MinimapConfig's title and sizePresets and EditableLinkLabel's editorTitle are now optional with no default value; left out, they are said in the localizations in scope. Their getters still answer the host's string or the English.
  • A MinimapConfig given title: 'Minimap' or the default presets explicitly is no longer == to one given nothing, since only the second is translated.

1.1.0 #

The editor's own words can be translated, and its menus can be read without reading English. Additive: a host that changes nothing sees exactly what 1.0.0 showed, and documents are unchanged.

Added #

  • NodeEditorLocalizations: the words of the built-in menus, the three dialogs, the minimap's bar and gear, and an empty comment's hint, in the pattern of MaterialLocalizations. NodeEditorLocalizations.of(context) falls back to DefaultNodeEditorLocalizations (English), so nothing has to be set up. Translate by extending the default and listing a delegate for it in localizationsDelegates.
  • DefaultNodeEditorLocalizations.delegate, the English for every locale.
  • NodeMenuRequest.localizations (const DefaultNodeEditorLocalizations()), which the editor fills from the context.
  • NodeMenuEntry.id (null) and NodeMenuEntryId, one per built-in entry; a build hook matches on it rather than on a label, and copyWith keeps it.

Changed #

  • The title of showNodeDescription, showGroupNameEditor and showConnectionLabelEditor is now String?; null takes the localised title. Callers that pass one are unaffected.

1.0.0 #

The API is stable from here. A document saved by 0.5.0 reads unchanged: the envelope is still version 1 and every key is the same, because the names that changed are Dart's, and the ones that are also the file's — a node's type, a port's kind and dataType — were kept for exactly that reason.

Renamed #

  • A prototype is a definition. NodePrototype was never a template stamped out once; it is a rule a node is resolved against for as long as it exists, which its own documentation said. Everything named after it follows.
0.5.0 1.0.0
NodePrototype NodeDefinition
LinkPrototype LinkDefinition
NodePrototypeRegistry NodeDefinitionRegistry
PrototypeResolution DefinitionResolution
PrototypeDivergenceHandler DefinitionDivergenceHandler
registry.linkPrototype(type) registry.linkDefinition(type)
NodeEditorController(prototypes:), controller.prototypes definitions:, controller.definitions
NodeGraphCodec(prototypes:), ConnectionsPainter.prototypes definitions
(graph, from, to) => … as a ConnectionValidator (check) => check.allowedByDefault && …
NodeEditorController.defaultConnectionValidator ConnectionCheck.allowedByDefault

Wiring #

  • BREAKING: a connection validator is handed the default rather than replaced by it. ConnectionValidator takes a ConnectionCheck — both PortRefs, both NodePorts, the graph, and allowedByDefault, the package's own verdict on duplicates, maxConnections and portsCompatible, worked out only if read. Anding it in adds a rule; not reading it replaces the checks, which used to happen to every validator whether or not its author meant it. a validator adds to the default checks, or replaces them in test/controller_test.dart.
  • connectionValidator is a settable field (null), beside guard and onEdit, so a host whose rules change with a mode swaps it.
  • A pasted wire answers to the same rules as a drawn one. A fragment can come from clipboard text, so its wires are judged against the graph after resolution and admitted one at a time; a refused one stays behind and its nodes still arrive. a pasted wire is asked what a drawn one is in test/clipboard_test.dart.

Reading a node #

  • GraphNode.field<T>, fieldOr<T> and metadataValue<T> read data and metadata typed: null when a key is absent or holds something else, rather than a throw under a host reading a file somebody edited, and an int accepted as a double because 1.0 comes back as 1 from anything that is not the Dart VM. The resolution and execution contexts' field now follow the same rule. a node reads its stored values typed in test/model_test.dart.

Removed from the barrel #

  • arrowheadSize, arrowheadsPath, minimapShadeBands and minimapNodeColor: helpers the package's painters share. The painters, ConnectionLayout, ConnectionRouter, SpatialHashGrid and MinimapProjection stay public — the README's Layers table is a promise that each layer is usable on its own.

Documentation #

  • The README follows the order a new editor meets things — the model, the controller, bodies, ports, node types, wiring — then customisation, and keeps dynamic ports, execution and serialisation for an Advanced part.

The canvas #

  • A move can land on the grid the canvas draws. NodeEditorController.snapToGrid (bool, false) turns it on, and the step is not a number of its own — it is NodeEditorTheme.gridSpacing, which the editor reports to the controller, so a card lands on a line that is drawn. snapStep is the resolved scene-space step, 0 when nothing snaps or when no editor has mounted yet. A node's top-left corner is what lands, each dragged node rounds its own, and it reaches a drag, an arrow-key nudge, the corner grip and a dragged waypoint. GridSnap.axis and GridSnap.offset are the rounding, public so a host placing a node itself gets the same answer. An arrangement through applyLayout is untouched by it.
  • BREAKING: NodeEditorTheme.snapToGrid is gone. It was a second number unrelated to the drawn grid. Set controller.snapToGrid instead; to keep a step that is not the spacing, change gridSpacing.
  • BREAKING: moveNodes, translateNodes and moveWaypoint no longer take snap. The first two consult snapToGrid themselves and take snap: false to place a node exactly — which is what Shift and an arrow key now do, since a one-unit nudge rounded to a cell would be a no-op. moveWaypoint takes its point as given: where a handle belongs depends on the connection style, so the editor resolves it.
  • A waypoint lined up with its neighbour stays lined up. With the grid on, an orthogonal handle takes a neighbour's row or column on whichever axes claim one and the grid on the rest — a corner with no jog beats a corner on a line.
  • A duplicate lands on the grid. NodeEditorClipboard.nudge is one cell on each axis while snapping is on and pasteNudge (32) otherwise; duplicate({Offset? offset}) takes null for it, where it used to default to pasteNudge.
  • A wire carried by a drag keeps its route when the grid is on. Both ends moving used to mean both ends moving in the same frame, which snapping makes almost never true.

A card can explain itself #

  • NodePort.description and NodeField.description say what one wire carries and what one control on the card does, beside the definition's own description. A host with a documentation page no longer keeps that prose in two places. A port's is outside ==, hashCode and the codec, on purpose: a port is serialised and resolution keeps the node it has when the ports it would build compare equal, so comparing prose would rewrite every node in every document written before the prose existed. A field's is neither serialised nor compared-away, because a field declaration never leaves the definition.
  • NodeEditor.descriptionBuilder renders the "About this node" dialog's body. The default is still selectable plain text; a host whose descriptions are Markdown passes its own body, with its own styles and link handling. The package gains no dependency — what a description is written in is the host's decision, not the package's.

0.5.0 #

Ports #

  • A control port and a data port are different shapes. NodeEditorTheme.controlPortShape and dataPortShape pick from PortShape.circle, triangle and diamond, one choice per kind for the whole canvas. The default is a triangle for control and a dot for data, so a row reads without tracing a wire; set both to circle for the old appearance.
  • NodeEditor.portTooltip labels a handle the pointer rests on. The package asks the host what a port carries, because a dataType is a tag the host chose and only the host knows what it is called out loud. Returning null or an empty string says nothing for that port.

0.4.0 #

Wires #

  • Wires can be routed through waypoints. Double-click a wire to add a handle on the curve, drag the handle to route the wire, double-click it to remove it; the wire's context menu offers Add waypoint here, Remove waypoint and Clear waypoints. NodeConnection.waypoints (List<Offset>, const []), with setConnectionWaypoints, insertWaypoint, moveWaypoint and removeWaypoint on the controller and GraphEditKind.routeConnection for guard and onEdit. A drag that moves both ends of a wire carries its route; applyLayout clears the routes of the wires it moves; a copy pastes with its route. Written to the document as an optional waypoints key, without a format version bump — an older build reads the wire and drops the route on its next save. NodeEditorTheme.waypointRadius (4.5) and waypointHitRadius (9); NodeMenuConnectionTarget gains waypoint and insertion.
  • Wires can be drawn in right angles. NodeEditorTheme.connectionStyle (ConnectionStyle, curved) — orthogonal draws every wire as axis-aligned legs with rounded corners; a waypoint is then a corner, and a dragged handle snaps onto the row or column of its neighbours (waypointAlignSnap, 6 screen px). connectionStub (24) is how far a wire runs straight out of a port before it may turn, connectionCornerRadius (8) the arc at each corner. Neither style routes around cards. ConnectionPath.build takes style, via, stub and cornerRadius; ConnectionPath.nearestOnRoute, ConnectionSpan, CubicSegment, PolylineSpan, RoutePoint and OrthogonalRoute are public.
  • A drag held against the edge of the canvas scrolls it, for a wire, a node, a group or a waypoint handle. NodeEditor.edgeScroll (EdgeScrollConfig?, const EdgeScrollConfig()) with margin (40) and speed (600 px/s), both in screen pixels; null turns it off.

Nodes #

  • A node body is told which of its ports are wired. NodeRenderState.connectedPorts (Set<String>, const {}) and isWired(portId), so a body can show an editor for a value only while no wire supplies it. Drawing a wire rebuilds the two bodies it touches and no others.
  • Every node carries metadata of its own. GraphNode.metadata (Map<String, Object?>, const {}), beside data rather than in it, so a prototype's pruning of data cannot take a user's notes away. copyWith(metadata:), NodeEditorController.setNodeMetadata(id, map) (reported as updateNodes; an equal map is not an edit). Written to the document only when non-empty, without a format version bump.

0.3.0 #

The first release since 0.1.0. 0.2.0 was never published; documents stamped fl_nodes_v2/0.2.0 were written by an unreleased build in between.

Watching a run #

  • NodeEditorRunner.onEvent (GraphRunListener?, null) is handed a GraphRunEvent for every step of a run — RunStarted, NodeStarted, NodeFinished, MemoHit, DiagnosticRaised, LogEmitted, RunFinished — a sealed hierarchy, fired synchronously before the notification that follows. A listener that throws is reported and the run goes on.
  • NodeEditorRunner.tracePayloads (bool, false) decides whether a GraphTraceValue carries the value on the wire; off, it is a WithheldPayload that still names the ports and the dataType tag. A wire nothing was written on is AbsentPayload.
  • NodeExecutionContext.log(message, {level, data}) lands on GraphRun.log and reaches the listener as LogEmitted.
  • GraphRun.id climbs per run; every event carries it, its sequence and at. GraphRunRecorder is a list-backed listener for tests and demos.

Documents #

  • NodeGraphCodec.schemaVersion (int?, null) and schemaMigrations (Map<int, GraphDocumentMigration>, empty) — the host's own version axis for what a node's type and data mean, stamped as schema and gated the way version is. A missing schema reads as 1; a null schemaVersion carries the key through untouched.
  • GraphDocumentMigrations.upgrade gains label and key, so a gap in either chain names its axis.

Nodes #

  • A node's corner can be dragged when its prototype says NodePrototype.resizable (bool, false), within minWidth, maxWidth and maxHeight. The height set this way is a floor, GraphNode.minHeight (double?, null), written to the document once set; ports stay on their rows (NodeEditorLayout.anchorSizeOf). One undo step per drag.
  • NodeEditorController.applyLayout(GraphLayout, {recordHistory}) places a whole arrangement in one edit, ignoring draggable; returns whether anything moved. NodeEditorLayout.onMeasured (VoidCallback?, null) fires once when every node has been measured.
  • NodeEditorController.guard (GraphEditGuard?, null) can refuse an edit before it lands; onEdit (GraphEditListener?, null) is told what landed, as a GraphEdit with a GraphEditKind and the ids it touched. Undo and redo pass through neither.
  • NodeEditorController.emphasis — a focus: a GraphEmphasis of nodes and connections that stand clear of a scrim washed over everything else, outside the document and the undo history. NodeEditorTheme.scrimColor (null, derived from background), scrimOpacity (0.62), emphasisInset (7), emphasisRadius (Radius.circular(14)).
  • ConnectionLabel.maxWidth (160) is public, so a layout can leave room for a caption.
  • Submenus are NodeSubmenuButton: a cascade opens to the right of its row, slides up only as far as the window demands, and never flips above the row it opened from. The whole tree opens one way, CascadeSide, chosen from its widest chain.
  • With createOnDrop on, the wire stays drawn while the Create menu is up and goes when the menu closes.
  • The minimap's gear and fold buttons sit against the panel's edge.

0.1.0 #

First release: a node graph editor for Flutter, the successor to fl_nodes, rebuilt so that a node body is an ordinary widget — text fields, sliders and forms all work inside one.

  • Immutable NodeGraph of GraphNodes, NodePorts and NodeConnections, edited through a NodeEditorController with history, selection, camera, layout, clipboard, project and runner subsystems.
  • Ports declare a PortKind (data or control) and an optional dataType; the default validator refuses to wire across either.
  • Prototypes as reduction rules: given a node's fields and wiring, they return the ports, fields and height it should have. Static and dynamic port families; foreign ports left alone.
  • Execution: controller.runner.run() walks control flow depth-first and pulls data inputs on demand, memoised per run; NodeExecutionContext.enteredVia names the control input the flow arrived on; a GraphRun carries the trace, values, diagnostics and failure.
  • Pan, zoom, marquee select, node drag, port-to-port wiring, connection captions, keyboard shortcuts and undo. Context menus on node, port, wire and canvas, built from MenuAnchor with entries as data and a build hook. Optional drop-to-create.
  • Comments: free-standing notes sharing the nodes' paint order, typed edits folding into one undo step per run.
  • Groups: named frames behind a set of nodes (Ctrl+G), moved by their handle, floating to the front with their members when selected.
  • Minimap: NodeEditor.minimap (MinimapConfig?, null) draws a movable, foldable readout over the canvas; MinimapController holds its size, maxScale (0.2) and idleOpacity (0.28) for a host to persist.
  • Rendering: connections, ports, grid and overlays painted rather than built; per-node widget slots, an incremental connection layout, a spatial hash for hit testing and culling, a fragment-shader grid with a CPU fallback.
  • NodeGraphCodec reads and writes a versioned JSON document with pluggable payload codecs and map-to-map migrations; a project subsystem keeps I/O behind a host-supplied source and sink.
5
likes
160
points
901
downloads

Documentation

API reference

Publisher

verified publisherconnecting-the-dots.dev

Weekly Downloads

A node graph editor for Flutter: pan, zoom, wire and run graphs, with ordinary Flutter widgets inside every node.

Repository (GitHub)
View/report issues

Funding

Consider supporting this project:

buymeacoffee.com

License

MIT (license)

Dependencies

flutter

More

Packages that depend on fl_nodes_v2