Skip to content

Diagram and controller ​

Two objects hold all diagram state. A Diagram is the shared thing: the committed document and its geometry, one undo history, its participants and its immutable DiagramConfiguration: the installed extensions, canvas bounds, fonts, rules and license. A DiagramController is one actor on a diagram, such as a person at a canvas, an agent or a script. It owns its selection, active tool, camera and in-progress edit, and every verb that edits the diagram.

Content verbs work without a canvas; camera and visual-effect members use the mounted DiagramCanvas. See collaboration for several actors on one diagram.

For every member and verb in one place, grouped by purpose, see the controller reference.

On this page ​

CategoryWhat you can do
ConstructionCreate a controller with its own diagram, or join an existing one.
Multiple actorsShare a diagram, undo per actor and respect element leases.
Reading stateRead the live view or the committed document.
VerbsPerform, check and discover edits: create, style, transform, connect and remove.
Selection, camera and toolSelection, camera, tool, creation styles and options.
BatchGroup verbs into one undo step.
InteractionsPreview a continuous edit, then commit or cancel it.
DiscoveryDescribe an element and edit its inspector properties.
Extension verbsHow verb implementations submit their edits.
Observing changesWatch state, listen to the controller or to committed changes.
Navigation and motionShow targets and play effects on a mounted canvas.
Lifetime and errorsDispose safely and handle denied edits.

Construction ​

dart
DiagramController({
  DiagramConfiguration? configuration,
  DiagramDocument? document,
  Diagram? diagram,
  DiagramParticipant participant = DiagramParticipant.local,
})

A diagram is configured in one place, its DiagramConfiguration; see configuration. Without diagram, a controller creates and owns a new Diagram from document and configuration. Disposing that controller disposes the diagram and every other controller on it.

dart
final controller = DiagramController(
  document: DiagramDocument.empty('my-drawing'),
  configuration: DiagramConfiguration(
    extensions: [EditorExtension(), GridExtension(visible: true, snap: true)],
  ),
);
final id = controller.createShape(Shapes.rectangle, at: DiagramPoint.zero);
controller.move(const DiagramPoint(40, 20), ids: [id]);
final json = controller.save(DiagramJsonCodec.new);
controller.dispose();

With diagram, the controller joins an existing diagram as another actor. Supplying document or configuration as well throws ArgumentError, because the diagram already has them.

dart
final agent = DiagramController(
  diagram: controller.diagram,
  participant: DiagramParticipant.agent(id: 'agent', name: 'Agent'),
);
ArgumentTypeDefault / behavior
diagramDiagram?Joins this diagram. When omitted, the controller creates and owns one.
documentDiagramDocument?A new empty drawing with a unique ID when omitted.
configurationDiagramConfiguration?The new diagram's extensions, canvas bounds, fonts, rules and license; an empty configuration when omitted. See configuration.
participantDiagramParticipantThe actor this controller edits as: DiagramParticipant({id, name, kind, color}), DiagramParticipant.agent(id:, name:), or the default DiagramParticipant.local (id local, name You). Its id must be nonempty and unique on the diagram; attribution, not authentication.

Canvas bounds are part of the configuration: DiagramConfiguration(bounds: CanvasBounds.fit(padding: 64)); see canvas bounds.

To open saved JSON as a new controller, use DiagramController.open(json, format: DiagramJsonCodec.new, configuration: DiagramConfiguration(extensions: [...])), which also takes participant. The format decodes over the installed extensions' codecs, so it covers the content and text they own. Opening validates the content without creating an undo entry. See persistence for loading into a live controller.

Every installed tool the grammar supports is available to setTool; the default tool is select. A canvas edits only when an editable extension such as EditorExtension() is installed. For accurate headless geometry install an extension carrying a font-aware textLayout; Flutter supplies text layout automatically. See headless text.

The diagram ​

MemberTypeMeaning
Diagram({document, configuration})Creates a diagram directly, for several controllers to share; its creator disposes it.
document, sceneDiagramDocument, ResolvedSceneThe last committed content and geometry, without any controller's preview.
snapshotDiagramSnapshotCommitted document and matching geometry as one value, for saving and output.
extensionsList<DiagramExtension>Installed extensions: the core defaults an application extension does not replace, then the application's extensions and their dependencies.
registryDiagramExtensionRegistryThe assembled shapes, tools, verbs, codecs and options of extensions. DiagramExtensionRegistry is in extension_api.dart.
configurationDiagramConfigurationThe shared configuration: extensions, bounds, fonts, rules and license. Observable.
setRules(rules)Replaces configuration.rules for every later edit.
resolverDiagramResolverThe installed grammar (resolver.registry) and text layout. DiagramResolver is in adapter.dart.
controllersList<DiagramController>Attached controllers, in attachment order.
participantsList<DiagramParticipant>The participants of the attached controllers, in attachment order; for presence.
participantById(id)DiagramParticipant?The participant with id, attached now or earlier, or null. Resolves the ids history entries and changes record.
refreshLayout()voidRe-resolves geometry after the host's text backend changes fonts or layout revision. Content and history are unchanged.
changesValueListenable<DiagramDocumentChange?>Committed changes from every controller; see observing changes.
isDisposed / dispose()Disposing a diagram disposes every controller on it.

Mounted Flutter canvases observe font loading automatically. Headless hosts call diagram.refreshLayout() after preparing fonts; it does not download fonts itself.

Multiple actors ​

Every controller on a diagram edits the same committed document and records into the one diagram history.

  • Per-actor undo. undo() and redo() replay only the calling controller's own edits. Another actor's edits remain.
  • Own participant state. Selection, active tool, camera and creation styles belong to each controller.
  • Element leases. While one controller previews an interaction, such as a drag, other controllers' edits that touch the elements it changes are denied with DiagramDenialCode.conflict and a reason naming the holder. Edits to other elements proceed, and the preview is rebased onto them. When the holder commits or cancels, the lease ends.
  • Shared configuration. diagram.setRules, controller.setBounds, controller.setFonts, controller.setDefaultFontFamily, controller.setOptions and refreshLayout apply to every controller and refresh their previews.
dart
final human = DiagramController(document: document);
final agent = DiagramController(
  diagram: human.diagram,
  participant: DiagramParticipant.agent(id: 'agent', name: 'Agent'),
);

agent.batch('Add review', () {
  final review = agent
      .createShape(Shapes.card, at: const DiagramPoint(400, 0))
      .id!;
  agent.setTitle(review, 'Review').requireAllowed();
});
human.undo(); // undoes only the human's own edits

A controller mounts one canvas. Each simultaneous canvas needs its own controller on the same diagram. Controllers share a diagram in one process; remote collaboration needs its own transport.

Reading state ​

A controller reads what its actor sees, including its own in-progress preview. The diagram reads committed content that every actor shares. The two are equal unless this controller holds an active interaction or batch.

Controller memberTypeMeaning
diagramDiagramThe shared diagram.
participantDiagramParticipantThe actor whose edits this controller attributes, by participant.id.
documentDiagramDocumentLive content, including this controller's preview.
sceneResolvedSceneLive geometry matching document.
elementById(id)DiagramElement?One element as this actor sees it.
selectionDiagramSelectionCurrent selection.
activeTool / toolsDiagramTool / DiagramToolRegistryCurrent tool and the tools this controller can activate (tools.definitions).
cameraDiagramCameraStateThis actor's viewport.
canUndo, canRedoboolWhether this controller has an edit to undo or redo.
isInteractingboolWhether this controller holds an interaction.
configurationDiagramConfigurationThe diagram's shared configuration: extensions, bounds, fonts, default font family, rules and license.
boundsCanvasBoundsHow far the canvas extends: CanvasBounds.infinite or CanvasBounds.fit(padding:).
options<T>()TThe current options of type T, such as GridOptions, shared by every controller on the diagram. StateError when no installed extension owns T.
isDisposed, isAttachedboolLifecycle, and whether a canvas is mounted.

Availability of an edit on the current selection comes from the verbs: controller.canPerform(ids, 'group').allowed, or controller.verb(ids, 'align')?.isAvailable for a control that also needs the verb's label and parameters.

Save, export and synchronize from diagram.document or diagram.snapshot, so an edit that may still be cancelled is excluded. exportSvg and exportPng already capture the committed snapshot.

A DiagramDocument provides elements, elementsById, elementById, shapeById, connectorById and textById. Keeping a document retains that snapshot, not a live view of later edits; change content through verbs.

An element's saved frame describes its authored size. Text that grows to fit its content can have larger visible bounds; read diagram.scene.elementById(id)?.bounds, and use one captured snapshot when combining content and geometry.

Verbs ​

A verb is a named, typed edit, and the only way anything changes a diagram. Canvas gestures, the inspector, overlays, key bindings, agents and your code all perform verbs. Every verb applies the diagram's admission and rules, records one undo entry. Direct calls complete or throw on refusal; calls through controller.edits return an EditResult. See verbs for discovery, symmetry and writing your own.

MemberReturnsBehavior
perform(ids, verb, [args])EditResultPerforms verb on ids as one undoable edit. No ids targets the diagram, as for createShape.
canPerform(ids, verb, [args])EditResultWhether perform would run, and why not. Nothing is published.
verbs([ids]) / verb(ids, name)List<DiagramVerbDescription> / DiagramVerbDescription?The verbs available on ids, with parameter schemas and availability.
dart
controller.perform([id], 'setOpacity', {'opacity': .5});
if (controller.canPerform([id], 'setLocked', {'locked': true}).allowed) {
  controller.edits.setLocked(true, ids: [id]);
}

The tables below list the typed extensions core ships on DiagramController; each is a thin wrapper over perform with the verb of the same name unless noted. Creating verbs report the new identity in the result's id. Verbs that act on several elements take a named ids: parameter and default to the current selection when it is omitted; verbs that act on one element take its identity first. Every verb also accepts an expectedRevision argument; see results.

Create ​

VerbidBehavior
createShape(type, {required at, id, size, style, values, text, ...})shapeRegistry defaults, parent discovery; full arguments.
createText({type, required at, text, ...})text elementStandalone text; full arguments.
createPath, createPolyline, createLinepath shapeEditable paths from authored nodes or world points.
createConnected({required source, required element, ...})connectorA target and its incoming connector together; the element keeps the identity you gave it. Creation.
add(element, {select})elementInserts an authored record without creation defaults. A connector added this way is admitted by the canConnect rule.
allocateId(prefix)—Returns a fresh identity unused in the document; not a verb.
dart
final result = controller.edits.createShape(Shapes.card, at: DiagramPoint.zero);
if (result.allowed) controller.edits.setTitle(result.id!, 'Orders');

Structure and clipboard ​

VerbBehavior
delete({ids})Removes elements with owned contents and bound connectors. Without ids, deletes the selection: elements, a connector label, path points or structured content.
group({ids}) / ungroup({ids})Wraps siblings in a group, or removes groups while keeping children.
frame({ids}) / unframe({ids})Wraps siblings in a frame, or unwraps frames while keeping their contents.
reparent({ids, parentId, delta})Moves elements under another owner, or to the root.
setFrameCollapsed(id, collapsed)Collapses or expands a frame.
copy({ids}) / paste(payload, {offset}) / duplicate({ids, offset})copy captures an immutable DiagramClipboardPayload; paste and duplicate allocate fresh identities and remap internal references. The host owns clipboard storage.

Group boundaries enclose their current children, including during previews and after undo or redo. Frames keep their authored size.

Transform and arrange ​

VerbBehavior
move(delta, {ids})Translates roots and owned contents once.
resize(size, {ids, fromCenter, preserveAspectRatio})Resizes through registered minima.
rotate({ids, required pivot, required radians})Rotates around a world pivot.
setElementBounds(id, rect)Places an element at an unrotated frame, resizing and moving it as one step.
align(alignment, {ids}) / distribute(distribution, {ids}) / reorder(order, {ids})Arrange elements. An already satisfied arrangement adds no history.
setZIndex(zIndex, {ids})Sets the paint order that reorder derives.
arrangeLanes({ids, direction, laneSize, gap})Lays swimlane frames out in rows or columns.
layout(layout, {nodeIds}) / cancelLayout()Automatic layout, such as Layout.tree(), with the installed algorithm for it; see layout.

Appearance, values and type ​

VerbBehavior
setStyle({ids, fill, stroke, strokeWidth, cornerRadius, strokeStyle, inkPreset})Changes only the given parts of each shape's appearance.
setOpacity(opacity, {ids}) / setVisible(visible, {ids}) / setLocked(locked, {ids})Whole-element opacity (0–1), visibility and locking. Locked owners protect descendants.
setAspectLocked(locked, {ids, square}) / setPorts(ports, {ids})Aspect lock, and instance ports on shapes that accept them.
setValues(id, patch)Patches registered values, validated by the shape's schema; values.
setImage(id, source)Sets image content, or removes it with null; image content.
setClipContent(clip, {ids}) / setPolygonSides(sides, {ids})Content clipping, and the side count of polygon shapes.
setType(id, type)Converts a shape to another registered type, keeping frame, style, ownership and matching text.

Text and slots ​

VerbBehavior
setText(id, text, {slot, storyId})Replaces text with plain paragraphs, one per line. slot names a shape's text part, such as a card's title (the verb title.setText); storyId picks a connector label.
setTextAlignment(id, alignment, {storyId, start, end}) / setTextDirection(...)Paragraph alignment or direction for a story or range.
perform([id], 'replaceText', {'before': before, 'after': after})Replaces a story that keeps its identity; setText is the typed text setter.
addSlot(id, slot, {text}) / removeSlot(id, slot)Shows or hides an optional slot, such as a card's tag.
setTitle(id, title) / setDescription(id, text) / setTag(id, tag)A card's own verbs; a null tag hides it.

Extension packages add verbs of their own: the Rich Text extension adds setMark, setHeading, setList and related story verbs (see rich text), the Table extension adds insertRow, mergeCells, setCellText and related table part verbs (see tables), and the shapes extension adds showSecondarySection for entities.

Connectors and paths ​

VerbBehavior
connect(from, to, {fromPort, toPort, type, route, select})Connects existing elements; id is the connector.
reconnect(id, {required atStart, required endpoint})Moves one end, admitted by the canConnect rule.
setConnectorStyle(id, {color, strokeWidth, strokeStyle}) / setStartArrowhead(id, {kind, width, height}) / setEndArrowhead(...)Line appearance, and each end's arrowhead; see connectors.
setConnectorRoute(id, {route, portSpacing, options}) / setRouteGeometry(id, {route, bend, controlPoints, routedPoints, preserveRoute}) / reroute({ids})Route kind, edited route geometry and obstacle-avoiding reroutes; reroute defaults to every connector.
addConnectorLabel(id, {text, story, fraction, offset, size}) / setConnectorLabel(id, labelId, {...}) / removeConnectorLabel(id, labelId) / arrangeLabels({ids, spacing})Connector labels; addConnectorLabel's id is the label, and arrangeLabels defaults to every connector.
setPathClosed, addPathPoint(id, {required after, t, id}), removePathNodes, setPathHandleMode, setPathSegmentKind, movePathControlEdit authored path nodes and handles. addPathPoint splits the segment after a node; its id is the new node.

History, persistence and output ​

MemberBehavior
undo() / redo()This controller's own latest edit. Not available inside a batch. Undo is history, not a reverse verb.
save(format)Encodes document in format, such as DiagramJsonCodec.new.
load(data, {format, expectedRevision, label})Replaces the document as one undoable edit. With format, decodes data first; without it, data is a DiagramDocument. Invalid input leaves the document unchanged; a stale expectedRevision is denied with conflict. See persistence.
exportSvg(options, {imageProvider}) / exportPng(options, {imageProvider})Captures the committed snapshot and exports asynchronously; see output.

Selection, camera and tool ​

Selection, camera, active tool, creation styles and options are not part of the document or its history, so they are not verbs. They change through plain controller methods that are not undoable and are not admitted by the diagram's rules.

MemberBehavior
select(ids) / selectAll({ownerId}) / clearSelection() / setSelection(selection)Change this actor's selection. Missing and non-selectable elements are filtered.
setSelection(ConnectorLabelSelection(connectorId: c, labelId: l)) / setSelection(PathPointSelection(elementId: e, nodeIds: {...}))Select one connector label, or path nodes.
setTool(tool)Activates a registered tool. Rejected during an interaction.
setCamera(camera)Moves this actor's viewport.
setShapeCreationStyle(type, style)Appearance for future shapes of a type; read it with controller.shapeCreationStyle(type).
setConnectorCreationRoute(route)Route kind for connectors drawn next.
setBounds(bounds)Replaces the canvas bounds for every controller on the diagram and re-clamps every camera. See canvas bounds.
setFonts(fonts) / setDefaultFontFamily(family)Replace the font choices or the default family for every controller on the diagram; a new default family lays text out again. See fonts.
setOptions(value)Replaces the options of the installed extension that owns value.runtimeType, such as GridOptions, for every controller on the diagram. Denied when no installed extension owns them. See extensions and options.
dart
controller.select([id]);
controller.edits.setOptions(
  controller.options<GridOptions>().copyWith(visible: true),
);

Batch ​

batch(label, build) runs build, whose verbs publish as one undo step. It returns the batch's EditResult.

dart
final result = controller.edits.batch('Add pair', () {
  final a = controller.edits.createShape(Shapes.rectangle, at: DiagramPoint.zero);
  final b = controller.edits.createShape(
    Shapes.rectangle,
    at: const DiagramPoint(200, 0),
  );
  controller.edits.connect(a.id!, b.id!);
});

When admission denies a verb (a rule, a lock or another controller's lease), later verbs are skipped, earlier changes are withdrawn and that denial is returned. A verb refused before it submits anything, such as one naming a missing element, only returns its denial; call requireAllowed() on it to abandon the batch. Other exceptions withdraw the batch and propagate. A nested batch joins the outer one.

Interactions ​

beginInteraction(label, {cumulative}) starts a continuous edit, such as a drag or a slider, and returns a DiagramInteractionLease:

MemberBehavior
perform(ids, verb, [args])Previews a verb as controller.perform would apply it.
performAll(invocations, {selection})Previews several DiagramVerbInvocations together, optionally showing a selection.
commit()Records one undo entry from the start to the last preview.
cancel()Restores the start without a history entry.
isActiveWhether the lease can still preview or finish.

A gesture lease, the default, starts every preview over from the interaction's starting document, so previews never accumulate. A cumulative lease, such as a text composition, applies each preview on top of the last and yields: an unrelated edit, an undo or a new interaction commits it first.

dart
final lease = controller.beginInteraction('Drag');
lease.perform([id], 'move', {'delta': const DiagramPoint(20, 0)});
lease.perform([id], 'move', {'delta': const DiagramPoint(50, 0)});
lease.commit().requireAllowed(); // one entry: moved 50

controller.cancelInteraction() cancels whichever interaction the controller holds, such as one a canvas gesture started; it does nothing when idle.

Only this controller sees the preview through document and scene; other controllers and diagram.document see committed content. Other controllers cannot edit the elements the preview changes until it ends. Disposing a controller cancels its interaction.

Discovery ​

Code and agents can discover what an element accepts and edit whatever the inspector can.

MemberReturnsBehavior
verbs([ids])List<DiagramVerbDescription>The verbs on ids, with parameter schemas and availability; see verbs.
describe(id)DiagramElementAnatomy?Kind, type, text slots (id, text definition, maxLines, isOptional, hasText), value schema, ports, structured contents, connector labels and acceptsChildren. Null for a missing id.
properties({ids})DiagramPropertyFieldsProperties shared by the affected selection, or explicit ids. Typed keys retain each value's type; set is one revision-checked undoable edit.
resolvedElementById(id)ResolvedElement?Observe live geometry for one identity, including previews, without subscribing to unrelated scene changes.
checkDiagnostics({rules})DiagramDiagnosticsInstalled domain checks and optional additional checks, captured at one document snapshot.
fixDiagnostic(report, fix)EditResultApply a report's proposed verbs atomically; stale reports and partial failures cannot overwrite the document.
setProperty(ids, key, value)EditResultSets one property by key on every target as one undoable edit. Unknown keys are denied.
dart
final anatomy = controller.describe(card)!;
final optional = anatomy.slots.where((slot) => slot.isOptional);
final names = controller.verbs([card]).map((verb) => verb.name);
controller.setProperty([card], 'element-opacity', 50);
final fill = controller.properties(ids: [card]).require(ShapeProperties.fill);
fill.set(const DiagramColor(0xff2457ff)).requireAllowed();

Omit ids to use the current selection. A multi-selection offers only fields declared by every selected kind. valueState distinguishes shared, mixed and unavailable; choices and bounds intersect all selected declarations. Kind-owned properties can read different underlying values and still edit together through their own verb arguments. Resolve fields again after an edit: the snapshot revision is checked by default, including at gesture start.

DiagramPropertyField and DiagramPropertyGesture belong to the application import. Declare properties in ShapeDefinition(properties: [...]) through extension_api.dart; custom Flutter property editors receive DiagramPropertyField<T> with their declared value type.

To continue a connection with a registry-default shape and one undo entry:

dart
final next = controller.createConnectedShape(
  Shapes.card,
  source: BoundEndpoint(elementId: card, portId: 'right'),
  toPort: 'left',
  at: const DiagramPoint(400, 0),
);

Connection rules, shape grammar and revision guards apply to both new elements as one transaction. id first requires an allowed result and then a created identity, so callers do not need result.id!.

Extension verbs ​

Extensions contribute verbs through DiagramExtension(verbs: [...]) and shapes through ShapeDefinition(verbs: [...]); see writing verbs. A verb's perform receives a DiagramVerbCall and submits its edit with call.submit(transaction), which goes through the same admission and history as every other edit and joins an enclosing batch or lease. DiagramTransaction and its steps come from the engine integration library, package:vyuh_diagram_core/adapter.dart.

dart
import 'package:vyuh_diagram_core/adapter.dart';
import 'package:vyuh_diagram_flutter/extension_api.dart';
import 'package:vyuh_diagram_flutter/vyuh_diagram_flutter.dart';

final setApproved = DiagramVerb(
  id: 'setApproved',
  label: 'Approved',
  symmetry: DiagramVerbSymmetry.setter,
  target: DiagramVerbTarget.element,
  parameters: [DiagramVerbParameter.boolean('approved')],
  perform: (call) {
    final shape = call.document.shapeById(call.id)!;
    return call.submit(
      DiagramTransaction(
        label: 'Approve',
        steps: [
          ReplaceElementStep(
            before: shape,
            after: shape.copyWith(
              values: {...shape.values, 'approved': call.arg<bool>('approved')},
            ),
          ),
        ],
      ),
    );
  },
);

A setter that only replaces its targets is shorter as DiagramVerb.update. Text verbs build on call.updateText(label: ..., transform: ...). controller.edits.dispatch(transaction, {expectedRevision}) is the low-level hook under call.submit; application code performs verbs instead, and the repository's boundary check rejects raw transactions outside verb implementations.

Observing changes ​

Diagram state is observed two ways: widgets read the controller's observable getters inside an Observer, and document-change events arrive on diagram.changes.

MemberBehavior
document, scene, selection, activeTool, camera, canUndo, canRedo, isInteracting, isAttachedObservable. An Observer that reads them rebuilds when they change.
elementById(id)Observable per identity: an Observer that reads elementById('a') rebuilds only when a changes, including removal and restoration.
configuration, boundsObservable configuration and canvas bounds.
options<T>(), effects.playingObservable options and playing effects.
diagram.changesA ValueListenable<DiagramDocumentChange?> notified once per committed change from any controller, including undo and redo.
dart
Observer(
  builder: (_) => IconButton(
    onPressed: controller.canUndo ? () => controller.edits.undo() : null,
    icon: const Icon(Icons.undo),
  ),
);

final changes = controller.diagram.changes;
changes.addListener(() {
  final change = changes.value!;
  final author = controller.diagram.participantById(change.participantId);
  save(change.after, author: author?.name ?? change.participantId);
});

Observer comes from package:flutter_mobx. Outside Flutter, reaction and autorun from package:mobx observe the same getters.

A DiagramDocumentChange carries before and after documents, the matching scene, the participantId of the participant that made it (resolve it with diagram.participantById), origin, label and the changed identities. A gesture emits once, when it commits. Previews, cancellation, selection, camera and options changes and denied edits emit nothing. Listeners cannot edit synchronously during notification; queue a follow-up verb instead.

These Flutter members require a mounted canvas. See navigation and motion for targets, motion and effect options.

MemberReturnsBehavior
screenToWorld(point)DiagramPointConverts canvas-local screen coordinates to world coordinates.
globalToWorld(offset)DiagramPointConverts a Flutter global position through the laid-out canvas.
view.show(target, {motion})Future<bool>Jumps to a ViewTarget, or travels with motion. false for missing content or an interrupted move.
view.stop()voidInterrupts the current move.
effects.play(effect, {required on})EffectHandlePlays an effect along a connector, or inside or around a shape.
effects.stopAll({on}), effects.playing, effects.playingOn(id)Stop or list playing effects.

Lifetime and errors ​

Calls through controller.edits return an EditResult: inspect allowed or call requireAllowed(), which throws DiagramChangeDenied carrying the result. A denial has a stable DiagramDenialCode (policy, locked, unavailableTarget, invalidSelection, unsupportedOperation, conflict) and an optional reason. Host rules returning EditResult.denied(reason: ...) default to policy.

ConditionResult
Unknown verb, missing argument, or an argument of the wrong type, non-finite or outside its parameter's rangeDenial with unsupportedOperation.
Unknown target elementDenial with unavailableTarget.
Locked target, or a target inside a locked ownerDenial with locked, from admission.
Stale expectedRevision on any verb, load, dispatch or a property writeDenial with conflict; nothing is evaluated.
Edit touches an element another controller is previewingDenial with conflict, naming the holder.
Creation denied by a rule or admissionDenied result without an id.
Invalid input that is not a verb argument, such as createPolyline with fewer than two pointsArgumentError.
diagram with document or configuration, or a participant with an empty idArgumentError.
setOptions for a type no installed extension ownsDenial with unsupportedOperation.
A participant id already attached to the diagramStateError.
undo() or redo() inside a batchStateError.
Any verb after disposalStateError. Listener removal and repeated dispose() are safe.
Viewport member while no canvas is mountedStateError.
dispose() while its canvas is mountedStateError; unmount first.

dispose() cancels the controller's interaction and releases its subscriptions without notifying them; other controllers still observe the release. Disposing again, even from another controller's listener, does nothing. A controller that created its diagram disposes the diagram and every controller on it; a controller that joined one only leaves it. Capture diagram.snapshot before disposal to retain the document and its geometry.

dart
final controller = DiagramController(
  document: DiagramDocument.empty('my-drawing'),
);

void onChange() {
  // Persist controller.diagram.changes.value!.after or update host UI.
}

controller.diagram.changes.addListener(onChange);

// Return this widget inside a bounded layout:
final canvas = DiagramCanvas(controller: controller);

// After the canvas is mounted and laid out:
await controller.view.show(const ViewTarget.selection(), motion: Motion.smooth);

// After removing the canvas from the widget tree:
controller.diagram.changes.removeListener(onChange);
controller.dispose();