fl_nodes_v2 1.5.0
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 aMinimapPalette— 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. MinimapPaletteis exported, with its two built-in palettes,MinimapPalette.onDarkandMinimapPalette.onLight, andcopyWith, 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): pastNodeSketches.below(0.25), every node is drawn from aNodeSketchthe host describes — boxes (SketchBox, with an optionalborder), bars andSketchTextlaid 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: aListenablethat 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 aConnectionAppearance; any field left null keeps the connection's own colour or the theme's.ConnectionDash.dashedand.dottedare ready-made patterns. The minimap is unchanged.NodeDefinition.deletableandcopyable(bothtrue): 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 ownremoveNodesis 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 ownindex.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.barHeightis 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') andminimapSizePresetLabel(size)(the label fromMinimapConfig.defaultSizePresets), inDefaultNodeEditorLocalizations.MinimapConfig.titleIn(words)andsizePresetsIn(words), what the panel draws: the host's own, or the words given.EditableLinkLabel.editorTitleIn(words), andwords(const DefaultNodeEditorLocalizations()) onNodeDefinitionRegistry.editorTitleFor.
Changed #
MinimapConfig'stitleandsizePresetsandEditableLinkLabel'seditorTitleare 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
MinimapConfiggiventitle: '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 ofMaterialLocalizations.NodeEditorLocalizations.of(context)falls back toDefaultNodeEditorLocalizations(English), so nothing has to be set up. Translate by extending the default and listing a delegate for it inlocalizationsDelegates.DefaultNodeEditorLocalizations.delegate, the English for every locale.NodeMenuRequest.localizations(const DefaultNodeEditorLocalizations()), which the editor fills from the context.NodeMenuEntry.id(null) andNodeMenuEntryId, one per built-in entry; abuildhook matches on it rather than on a label, andcopyWithkeeps it.
Changed #
- The
titleofshowNodeDescription,showGroupNameEditorandshowConnectionLabelEditoris nowString?; 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.
NodePrototypewas 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.
ConnectionValidatortakes aConnectionCheck— bothPortRefs, bothNodePorts, the graph, andallowedByDefault, the package's own verdict on duplicates,maxConnectionsandportsCompatible, 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 intest/controller_test.dart. connectionValidatoris a settable field (null), besideguardandonEdit, 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>andmetadataValue<T>readdataandmetadatatyped: null when a key is absent or holds something else, rather than a throw under a host reading a file somebody edited, and anintaccepted as adoublebecause1.0comes back as1from anything that is not the Dart VM. The resolution and execution contexts'fieldnow follow the same rule. a node reads its stored values typed intest/model_test.dart.
Removed from the barrel #
arrowheadSize,arrowheadsPath,minimapShadeBandsandminimapNodeColor: helpers the package's painters share. The painters,ConnectionLayout,ConnectionRouter,SpatialHashGridandMinimapProjectionstay 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 isNodeEditorTheme.gridSpacing, which the editor reports to the controller, so a card lands on a line that is drawn.snapStepis the resolved scene-space step,0when 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.axisandGridSnap.offsetare the rounding, public so a host placing a node itself gets the same answer. An arrangement throughapplyLayoutis untouched by it. - BREAKING:
NodeEditorTheme.snapToGridis gone. It was a second number unrelated to the drawn grid. Setcontroller.snapToGridinstead; to keep a step that is not the spacing, changegridSpacing. - BREAKING:
moveNodes,translateNodesandmoveWaypointno longer takesnap. The first two consultsnapToGridthemselves and takesnap: falseto place a node exactly — which is whatShiftand an arrow key now do, since a one-unit nudge rounded to a cell would be a no-op.moveWaypointtakes 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.nudgeis one cell on each axis while snapping is on andpasteNudge(32) otherwise;duplicate({Offset? offset})takes null for it, where it used to default topasteNudge. - 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.descriptionandNodeField.descriptionsay what one wire carries and what one control on the card does, beside the definition's owndescription. A host with a documentation page no longer keeps that prose in two places. A port's is outside==,hashCodeand 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.descriptionBuilderrenders 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.controlPortShapeanddataPortShapepick fromPortShape.circle,triangleanddiamond, 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 tocirclefor the old appearance. NodeEditor.portTooltiplabels a handle the pointer rests on. The package asks the host what a port carries, because adataTypeis 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 []), withsetConnectionWaypoints,insertWaypoint,moveWaypointandremoveWaypointon the controller andGraphEditKind.routeConnectionforguardandonEdit. A drag that moves both ends of a wire carries its route;applyLayoutclears the routes of the wires it moves; a copy pastes with its route. Written to the document as an optionalwaypointskey, without a format version bump — an older build reads the wire and drops the route on its next save.NodeEditorTheme.waypointRadius(4.5) andwaypointHitRadius(9);NodeMenuConnectionTargetgainswaypointandinsertion. - Wires can be drawn in right angles.
NodeEditorTheme.connectionStyle(ConnectionStyle,curved) —orthogonaldraws 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.buildtakesstyle,via,stubandcornerRadius;ConnectionPath.nearestOnRoute,ConnectionSpan,CubicSegment,PolylineSpan,RoutePointandOrthogonalRouteare 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()) withmargin(40) andspeed(600 px/s), both in screen pixels;nullturns it off.
Nodes #
- A node body is told which of its ports are wired.
NodeRenderState.connectedPorts(Set<String>,const {}) andisWired(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 {}), besidedatarather than in it, so a prototype's pruning ofdatacannot take a user's notes away.copyWith(metadata:),NodeEditorController.setNodeMetadata(id, map)(reported asupdateNodes; 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 aGraphRunEventfor 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 aGraphTraceValuecarries the value on the wire; off, it is aWithheldPayloadthat still names the ports and thedataTypetag. A wire nothing was written on isAbsentPayload.NodeExecutionContext.log(message, {level, data})lands onGraphRun.logand reaches the listener asLogEmitted.GraphRun.idclimbs per run; every event carries it, itssequenceandat.GraphRunRecorderis a list-backed listener for tests and demos.
Documents #
NodeGraphCodec.schemaVersion(int?, null) andschemaMigrations(Map<int, GraphDocumentMigration>, empty) — the host's own version axis for what a node'stypeanddatamean, stamped asschemaand gated the wayversionis. A missingschemareads as 1; a nullschemaVersioncarries the key through untouched.GraphDocumentMigrations.upgradegainslabelandkey, 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), withinminWidth,maxWidthandmaxHeight. 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, ignoringdraggable; 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 aGraphEditwith aGraphEditKindand the ids it touched. Undo and redo pass through neither.NodeEditorController.emphasis— a focus: aGraphEmphasisof nodes and connections that stand clear of a scrim washed over everything else, outside the document and the undo history.NodeEditorTheme.scrimColor(null, derived frombackground),scrimOpacity(0.62),emphasisInset(7),emphasisRadius(Radius.circular(14)).ConnectionLabel.maxWidth(160) is public, so a layout can leave room for a caption.
Menus #
- 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
createOnDropon, 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
NodeGraphofGraphNodes,NodePorts andNodeConnections, edited through aNodeEditorControllerwithhistory,selection,camera,layout,clipboard,projectandrunnersubsystems. - Ports declare a
PortKind(dataorcontrol) and an optionaldataType; 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.enteredVianames the control input the flow arrived on; aGraphRuncarries 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
MenuAnchorwith 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;MinimapControllerholds its size,maxScale(0.2) andidleOpacity(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.
NodeGraphCodecreads and writes a versioned JSON document with pluggable payload codecs and map-to-map migrations; aprojectsubsystem keeps I/O behind a host-supplied source and sink.