fl_nodes_v2 1.2.2
fl_nodes_v2: ^1.2.2 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.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.